diff --git a/.ci/jobs.yml b/.ci/jobs.yml index fc3e80064fa6fa..89fce3cf488d59 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -14,7 +14,7 @@ JOB: - kibana-ciGroup10 - kibana-ciGroup11 - kibana-ciGroup12 - # - kibana-visualRegression + - kibana-visualRegression # make sure all x-pack-ciGroups are listed in test/scripts/jenkins_xpack_ci_group.sh - x-pack-firefoxSmoke @@ -28,7 +28,7 @@ JOB: - x-pack-ciGroup8 - x-pack-ciGroup9 - x-pack-ciGroup10 - # - x-pack-visualRegression + - x-pack-visualRegression # `~` is yaml for `null` exclude: ~ diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index 2c408a5cc1e7e5..4d2ae5d01688cc 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -43,3 +43,8 @@ Adjusting the sampling rate controls what percent of requests are traced. `1.0` means _all_ requests are traced. If you set the `TRANSACTION_SAMPLE_RATE` to a value below `1.0`, the agent will randomly sample only a subset of transactions. Unsampled transactions only record the name of the transaction, the overall transaction time, and the result. + +IMPORTANT: In a distributed trace, the sampling decision is propagated by the initializing Agent. +This means if you're using multiple agents, only the originating service's sampling rate will be used. +Be sure to set sensible defaults in _all_ of your agents, especially the +{apm-rum-ref}/configuration.html#transaction-sample-rate[JavaScript RUM Agent]. diff --git a/docs/development/core/server/kibana-plugin-server.ikibanasocket.authorizationerror.md b/docs/development/core/server/kibana-plugin-server.ikibanasocket.authorizationerror.md new file mode 100644 index 00000000000000..0629b8e2b9ade8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ikibanasocket.authorizationerror.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [authorizationError](./kibana-plugin-server.ikibanasocket.authorizationerror.md) + +## IKibanaSocket.authorizationError property + +The reason why the peer's certificate has not been verified. This property becomes available only when `authorized` is `false`. + +Signature: + +```typescript +readonly authorizationError?: Error; +``` diff --git a/docs/development/core/server/kibana-plugin-server.ikibanasocket.authorized.md b/docs/development/core/server/kibana-plugin-server.ikibanasocket.authorized.md new file mode 100644 index 00000000000000..abb68f8e8f0e0a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ikibanasocket.authorized.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [authorized](./kibana-plugin-server.ikibanasocket.authorized.md) + +## IKibanaSocket.authorized property + +Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is `undefined`. + +Signature: + +```typescript +readonly authorized?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.ikibanasocket.md b/docs/development/core/server/kibana-plugin-server.ikibanasocket.md index 129a3b1d2311ab..d100b1eb2bcd9f 100644 --- a/docs/development/core/server/kibana-plugin-server.ikibanasocket.md +++ b/docs/development/core/server/kibana-plugin-server.ikibanasocket.md @@ -12,6 +12,13 @@ A tiny abstraction for TCP socket. export interface IKibanaSocket ``` +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [authorizationError](./kibana-plugin-server.ikibanasocket.authorizationerror.md) | Error | The reason why the peer's certificate has not been verified. This property becomes available only when authorized is false. | +| [authorized](./kibana-plugin-server.ikibanasocket.authorized.md) | boolean | Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is undefined. | + ## Methods | Method | Description | diff --git a/docs/development/plugin/development-plugin-feature-registration.asciidoc b/docs/development/plugin/development-plugin-feature-registration.asciidoc index 7505b018c6fbd8..5a852515148614 100644 --- a/docs/development/plugin/development-plugin-feature-registration.asciidoc +++ b/docs/development/plugin/development-plugin-feature-registration.asciidoc @@ -1,15 +1,15 @@ [[development-plugin-feature-registration]] === Plugin feature registration -If your plugin will be used with {kib}’s default distribution, then you have the ability to register the features that your plugin provides. Features are typically apps in {kib}; once registered, you can toggle them via Spaces, and secure them via Roles when security is enabled. +If your plugin will be used with {kib}'s default distribution, then you have the ability to register the features that your plugin provides. Features are typically apps in {kib}; once registered, you can toggle them via Spaces, and secure them via Roles when security is enabled. ==== UI Capabilities -Registering features also gives your plugin access to “UI Capabilities”. These capabilities are boolean flags that you can use to conditionally render your interface, based on the current user’s permissions. For example, you can hide or disable a Save button if the current user is not authorized. +Registering features also gives your plugin access to “UI Capabilities”. These capabilities are boolean flags that you can use to conditionally render your interface, based on the current user's permissions. For example, you can hide or disable a Save button if the current user is not authorized. ==== Registering a feature -Feature registration is controlled via the built-in `xpack_main` plugin. To register a feature, call `xpack_main`'s `registerFeature` function from your plugin’s `init` function, and provide the appropriate details: +Feature registration is controlled via the built-in `xpack_main` plugin. To register a feature, call `xpack_main`'s `registerFeature` function from your plugin's `init` function, and provide the appropriate details: ["source","javascript"] ----------- @@ -42,7 +42,7 @@ Registering a feature consists of the following fields. For more information, co |`app` (required) |`string[]` |`["sample_app", "kibana"]` -|An array of applications this feature enables. Typically, all of your plugin’s apps (from `uiExports`) will be included here. +|An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here. |`privileges` (required) |{repo}blob/{branch}/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts[`FeatureWithAllOrReadPrivileges`]. @@ -72,7 +72,7 @@ To access capabilities, import them from `ui/capabilities`: ["source","javascript"] ----------- -import { uiCapabilities } from ‘ui/capabilities’; +import { uiCapabilities } from 'ui/capabilities'; const canUserSave = uiCapabilities.foo.save; if (canUserSave) { @@ -124,7 +124,7 @@ The `all` privilege defines a single “save” UI Capability. To access this in ["source","javascript"] ----------- -import { uiCapabilities } from ‘ui/capabilities’; +import { uiCapabilities } from 'ui/capabilities'; const canUserSave = uiCapabilities.canvas.save; if (canUserSave) { diff --git a/docs/infrastructure/getting-started.asciidoc b/docs/infrastructure/getting-started.asciidoc new file mode 100644 index 00000000000000..510d53ccdccc07 --- /dev/null +++ b/docs/infrastructure/getting-started.asciidoc @@ -0,0 +1,11 @@ +[role="xpack"] +[[xpack-metrics-getting-started]] +== Getting started with infrastructure monitoring + +To get started with the Infrastructure app in Kibana, you need to start collecting metrics data for your infrastructure. + +Kibana provides step-by-step instructions to help you add metrics data. +The {infra-guide}[Infrastructure Monitoring Guide] is a good source for more detailed information and instructions. + +[role="screenshot"] +image::infrastructure/images/metrics-add-data.png[Screenshot showing Add metric data to Kibana UI] diff --git a/docs/infrastructure/images/infra-sysmon.jpg b/docs/infrastructure/images/infra-sysmon.jpg deleted file mode 100644 index 36afcb82061cc9..00000000000000 Binary files a/docs/infrastructure/images/infra-sysmon.jpg and /dev/null differ diff --git a/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png new file mode 100644 index 00000000000000..5b82d8c9b4e194 Binary files /dev/null and b/docs/infrastructure/images/infra-sysmon.png differ diff --git a/docs/infrastructure/images/infra-view-metrics.png b/docs/infrastructure/images/infra-view-metrics.png new file mode 100644 index 00000000000000..d4ee287c234be6 Binary files /dev/null and b/docs/infrastructure/images/infra-view-metrics.png differ diff --git a/docs/infrastructure/images/metrics-add-data.png b/docs/infrastructure/images/metrics-add-data.png new file mode 100644 index 00000000000000..d9640e0d9f5da1 Binary files /dev/null and b/docs/infrastructure/images/metrics-add-data.png differ diff --git a/docs/infrastructure/images/time-filter-calendar.png b/docs/infrastructure/images/time-filter-calendar.png new file mode 100644 index 00000000000000..d0019c99fe8933 Binary files /dev/null and b/docs/infrastructure/images/time-filter-calendar.png differ diff --git a/docs/infrastructure/images/time-filter-clock.png b/docs/infrastructure/images/time-filter-clock.png new file mode 100644 index 00000000000000..fe8542aad41de0 Binary files /dev/null and b/docs/infrastructure/images/time-filter-clock.png differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc index c48d78d6cf3861..33389345fff770 100644 --- a/docs/infrastructure/index.asciidoc +++ b/docs/infrastructure/index.asciidoc @@ -4,83 +4,31 @@ [partintro] -- -Use the interactive Infrastructure UI to monitor your infrastructure and -identify problems in real time. You can explore metrics and logs for common -servers, containers, and services. +The Infrastructure app enables you to monitor your infrastructure and identify problems in real time. +You start with a visual summary of your infrastructure where you can view basic metrics for common servers, containers, and services. +Then you can drill down to view more detailed metrics or other information for that component. -[role="screenshot"] -image::infrastructure/images/infra-sysmon.jpg[Infrastructure Overview in Kibana] - - -[float] -== Add data - -Kibana provides step-by-step instructions to help you add log data. The -{infra-guide}[Infrastructure Monitoring Guide] is a good source for more -detailed information and instructions. +You can: -[float] -== Configure data sources +* View an inventory of your infrastructure by hosts, Kubernetes pod or Docker containers. +You can group and filter the data in various ways to help you identify the items that interest you. -The `metricbeat-*` index pattern is used to query the data by default. -If your metrics are located in a different set of indices, or use a -different timestamp field, you can adjust the source configuration via the user -interface or the {kib} configuration file. +* View current and historic values for metrics such as CPU usage, memory usage, and network traffic for each component. +The available metrics depend on the kind of component being inspected. -NOTE: Logs and Infrastructure share a common data source definition in -each space. Changes in one of them can influence the data displayed in the -other. - -[float] -=== Configure source - -Configure source can be accessed via the corresponding -image:logs/images/logs-configure-source-gear-icon.png[Configure source icon] -button in the toolbar: - -[role="screenshot"] -image::infrastructure/images/infrastructure-configure-source.png[Configure Infrastructure UI source button in Kibana] +* Use *Metrics Explorer* to group and visualize multiple customizable metrics for one or more components in a graphical format. +You can optionally save these views and add them to {kibana-ref}/dashboard.html[dashboards]. -This opens the source configuration fly-out dialog, in which the following -configuration items can be inspected and adjusted: +* Seamlessly switch to view the corresponding logs, application traces or uptime information for a component. -* *Name*: The name of the source configuration. -* *Indices*: The patterns of the elasticsearch indices to read metrics and logs - from. -* *Fields*: The names of particular fields in the indices that need to be known - to the Infrastructure and Logs UIs in order to query and interpret the data - correctly. +To get started, you need to <>. Then you can <>. [role="screenshot"] -image::infrastructure/images/infrastructure-configure-source-dialog.png[Configure Infrastructure UI source dialog in Kibana] - -TIP: If <> are enabled in your Kibana instance, any configuration -changes performed via Configure source are specific to that space. You can -therefore easily make different subsets of the data available by creating -multiple spaces with different data source configurations. - -[float] -[[infra-read-only-access]] -=== Read only access -When you have insufficient privileges to change the source configuration, the following -indicator in Kibana will be displayed. The buttons to change the source configuration -won't be visible. For more information on granting access to -Kibana see <>. - -[role="screenshot"] -image::infrastructure/images/read-only-badge.png[Example of Infrastructure's read only access indicator in Kibana's header] - - -[float] -=== Configuration file - -The settings in the configuration file are used as a fallback when no other -configuration for that space has been defined. They are located in the -configuration namespace `xpack.infra.sources.default`. See -<> for a complete list of the possible entries. +image::infrastructure/images/infra-sysmon.png[Infrastructure Overview in Kibana] -- -include::monitor.asciidoc[] +include::getting-started.asciidoc[] include::infra-ui.asciidoc[] +include::view-metrics.asciidoc[] include::metrics-explorer.asciidoc[] diff --git a/docs/infrastructure/infra-ui.asciidoc b/docs/infrastructure/infra-ui.asciidoc index a736d88971aba2..45066d8d8a6247 100644 --- a/docs/infrastructure/infra-ui.asciidoc +++ b/docs/infrastructure/infra-ui.asciidoc @@ -1,68 +1,111 @@ [role="xpack"] [[infra-ui]] -== Using the Infrastructure UI +== Using the Infrastructure app -Use the Infrastructure UI in {kib} to monitor your infrastructure and identify -problems in real time. +Use the Infrastructure app in {kib} to monitor your infrastructure and identify problems in real time. +You can explore metrics for hosts, containers, and services. +You can also drill down to view more detailed metrics, or seamlessly switch to view the corresponding logs, application traces, and uptime information. -You start with an overview of your infrastructure. -Then you can use the interactive UI to drill down into areas of interest. +Initially, the *Inventory* tab shows an overview of the hosts in of your infrastructure and the current CPU usage for each host. +From here, you can drill down into areas of interest. [role="screenshot"] -image::infrastructure/images/infra-sysmon.jpg[Infrastructure Overview in Kibana] - -TIP: Right-click an element to jump to related logs or metrics. +image::infrastructure/images/infra-sysmon.png[Infrastructure Overview in Kibana] [float] [[infra-cat]] -=== View your infrastructure by hosts or container - -Select the high-level view: *Hosts*, *Kubernetes*, or *Docker*. -When you change views, you can see the same data through the perspective of a -different category. +=== Choose the high-level view of your infrastructure -[float] -[[infra-search]] -=== Use the power of Search +Select the high-level view from *Hosts*, *Kubernetes*, or *Docker*. +When you change views, you see the same data through the perspective of a different category. -The Search bar is always available. You can use it to perform adhoc or structured searches. -The Search feature can surface the information you're looking for, even if the -other options you have selected would normally filter it out. +The default representation is the *Map view*, which shows your components in a _waffle map_ of one or more rectangular grids. +If the view you select has a large number of components, you can hover over a component to see the details for that component. Alternatively, if you would prefer to see your infrastructure as a table, click *Table view*. [float] [[infra-metric]] -=== Start at a high-level by selecting the metric +=== Select the metric to view -This filter helps you start focusing on potential problem areas that may need -further investigation. You'll see metrics that are most relevant for hosts or -the container you selected. +Select the metric to view from the *Metric* dropdown list. +The available metrics are those that are most relevant for the high-level view you selected. [float] [[infra-group]] -=== Group components +=== Group components + +Select the way you want to group the infrastructure components from the *Group By* dropdown list. +The available options are specific to your physical, virtual, or container-based infrastructure. +Examples of grouping options include *Availability Zone*, *Machine Type*, *Project ID*, and *Cloud Provider* for hosts, and *Namespace* and *Node* for Kubernetes. + +[float] +[[infra-search]] +=== Use the power of search -The *Group By* selector offers grouping options that are native and specific for -your physical, virtual, and container-based infrastructure. -Examples include Availability Zone, Machine Type, Project ID, and Cloud Provider -for Hosts, and Namespace and Node for Kubernetes. +Use the search bar to perform ad hoc or structured searches using {kibana-ref}/kuery-query.html[Kibana Query Language] which features autocomplete and a simple, easy to use syntax. +For example, enter `host.hostname : "host1"` to see only the information for `host1`. [float] [[infra-date]] -=== Specify the date range +=== Specify the time and date -Use the time selector to focus on a specific timeframe. +Click the time selector to choose the timeframe for the metrics. +The values shown are the values for the last minute at the specified time and date. [float] [[infra-refresh]] -=== Auto-refresh or pause +=== Auto-refresh metrics -Set auto-refresh to keep up-to-date information coming in, or stop -refreshing to focus on historical data without new distractions. +Select *Auto-refresh* to keep up-to-date metrics information coming in, or *Stop refreshing* to focus on historical data without new distractions. [float] [[infra-configure-source]] -=== Adapt to your metric source +=== Configure the data to use for your metrics + +The default source configuration for metrics is specified in the {kibana-ref}/infrastructure-ui-settings-kb.html[Infrastructure app settings] in the {kibana-ref}/settings.html[Kibana configuration file]. +The default configuration uses the `metricbeat-*` index pattern to query the data. +The default configuration also defines field settings for things like timestamps and container names. + +If your metrics have custom index patterns, or use non-default field settings, you can override the default settings. +Click *Configuration* to change the settings. +This opens the *Configure source* fly-out dialog. + +NOTE: These settings are shared with logs. Changes you make here may also affect the settings used in the *Logs* tab. + +In the *Configure source* dialog, you can change the following values: + +* *Name*: the name of the source configuration +* *Indices*: the index pattern or patterns in the Elasticsearch indices to read metrics data and log data + from +* *Fields*: the names of specific fields in the indices that need are used to query and interpret the data correctly + +TIP: If <> are enabled in your Kibana instance, any configuration changes you make here are specific to the current space. +You can make different subsets of data available by creating multiple spaces with different data source configurations. + +TIP: If you don't see the *Configuration* option, you may not have sufficient privileges to change the source configuration. +For more information see <>. + +[float] +[[infra-metrics-explorer]] +=== Visualize multiple metrics in Metrics Explorer + +<> allows you to visualize and analyze metrics for multiple components in a powerful and configurable way. Click the *Metrics Explorer* tab to get started. + +[float] +[[infra-drill-down]] +=== Drill down for related information + +Hover over a component to see more information about that component. + +Click a component to see the other actions available for that component. +You can: + +* Select *View Metrics* to <>. + +* Select *View Logs* to <> in the *Logs* app. + +Depending on the features you have installed and configured, you may also be able to: + +* Select *View APM* to <> in the *APM* app. + +* Select *View Uptime* to <> in the *Uptime* app. -Using a custom index pattern to store the metrics, or want to limit the entries -presented in a space? Use configure source to change the index pattern and -other settings. diff --git a/docs/infrastructure/metrics-explorer.asciidoc b/docs/infrastructure/metrics-explorer.asciidoc index 008f7ee1bacc6a..729d5ff97efd73 100644 --- a/docs/infrastructure/metrics-explorer.asciidoc +++ b/docs/infrastructure/metrics-explorer.asciidoc @@ -1,10 +1,10 @@ [role="xpack"] [[metrics-explorer]] - == Metrics Explorer -Metrics Explorer allows you to visualize metrics data collected by Metricbeat and group it in various ways to visualize multiple metrics. -It can be a starting point for further investigations. +Metrics Explorer in the Infrastructure app in Kibana allows you to group and visualise multiple customisable metrics for one or more components in a graphical format. +This can be a starting point for further investigations. +You can also save your views and add them to {kibana-ref}/dashboard.html[dashboards]. [role="screenshot"] image::infrastructure/images/metrics-explorer-screen.png[Metrics Explorer in Kibana] @@ -13,13 +13,13 @@ image::infrastructure/images/metrics-explorer-screen.png[Metrics Explorer in Kib [[metrics-explorer-requirements]] === Metrics Explorer requirements and considerations -* The Metrics Explorer uses data collected from {metricbeat-ref}/metricbeat-overview.html[Metricbeat]. -* You need read permissions on `metricbeat-*` or the metric index specified in the Infrastructure configuration UI. -* Metrics Explorer uses the timestamp field set in the Infrastructure configuration UI. +* Metrics Explorer uses data collected from {metricbeat-ref}/metricbeat-overview.html[Metricbeat]. +* You need read permissions on `metricbeat-*` or the metric index specified in the Infrastructure configuration. +* Metrics Explorer uses the timestamp field set in the Infrastructure configuration. By default that is set to `@timestamp`. * The interval for the X Axis is set to `auto`. The bucket size is determined by the time range. -* *Open in Visualize* requires you to have access to the Visualize app, otherwise it is not available. +* To use *Open in Visualize* you need access to the Visualize app. [float] [[metrics-explorer-tutorial]] @@ -49,7 +49,7 @@ If you only have metrics for a single host, you will see a single graph. Congratulations! Either way, you've explored your first metric. 5. Let's explore a bit further. -In the upper right hand corner of the graph for one of the hosts, select the *Actions* dropdown and click *Add Filter* to show ony the metrics for that host. +In the upper right hand corner of the graph for one of the hosts, select the *Actions* dropdown and click *Add Filter* to show only the metrics for that host. This adds a {kibana-ref}/kuery-query.html[Kibana Query Language] filter for `host.name` in the second row of the Metrics Explorer configuration. If you only have one host, the graph will not change as you are already exploring metrics for a single host. diff --git a/docs/infrastructure/monitor.asciidoc b/docs/infrastructure/monitor.asciidoc deleted file mode 100644 index 90b27460e89e45..00000000000000 --- a/docs/infrastructure/monitor.asciidoc +++ /dev/null @@ -1,35 +0,0 @@ -[role="xpack"] -[[monitor-infra]] -== Monitor your infrastructure - -You start with a visual summary of your infrastructure. -Then you can drill down into areas of interest. - -[float] -=== Monitor hosts and containers - -View your infrastructure by hosts or containers. The UI helps you group, filter, -and target data to help you find what you seek. - -[float] -=== View metrics - -View relevant metric information such as CPU usage, memory usage, load, and inbound or -outbound traffic. - -[float] -=== View logs - -View real-time and historical logs in the same viewer, streaming or pausing as needed. -When you stream logs, the most recent event appears at the bottom of console. -You can easily correlate metrics and logs. Right-click an element in the -Infrastructure UI, and select *View logs*. - - - - - - - - - diff --git a/docs/infrastructure/view-metrics.asciidoc b/docs/infrastructure/view-metrics.asciidoc new file mode 100644 index 00000000000000..fffa998b530fc7 --- /dev/null +++ b/docs/infrastructure/view-metrics.asciidoc @@ -0,0 +1,39 @@ +[role="xpack"] +[[xpack-view-metrics]] + +== Viewing infrastructure metrics + +When you select *View Metrics* for a component in your infrastructure from the <>, you can view detailed metrics for that component, and for any related components. + +[role="screenshot"] +image::infrastructure/images/infra-view-metrics.png[Infrastructure View Metrics in Kibana] + +[float] +[[infra-view-metrics-date]] +=== Specify the time and date range + +Use the time filter to select the time and date range for the metrics. + +To quickly select some popular time range options, click the calendar dropdown image:infrastructure/images/time-filter-calendar.png[]. In this popup you can choose from: + +* *Quick select* to choose a recent time range, and use the back and forward arrows to move through the time ranges +* *Commonly used* to choose a time range from some commonly used options such as *Last 15 minutes*, *Today*, or *Week to date* +* *Refresh every* to specify an auto-refresh rate + +NOTE: When you start auto-refresh from within this dialog, the calendar dropdown changes to a clock image:infrastructure/images/time-filter-clock.png[]. + +For complete control over the start and end times, click the start time or end time shown in the bar beside the calendar dropdown. In this popup, you can choose from the *Absolute*, *Relative* or *Now* tabs, then specify the required options. + +[float] +[[infra-view-refresh-metrics-date]] +=== Refresh the metrics + +You can click *Refresh* to manually refresh the metrics. + +[float] +[[infra-view-go-to-chart]] +=== Go to a specific chart + +The charts available for this component are shown in a list on the left of the page. Click a chart in the list to quickly go to that chart. + + diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index bfaee1b3ae16ee..e06b920e9d6842 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -24,3 +24,9 @@ For more {kib} configuration settings, see <>. == Uptime security This page has moved. Please see the new section in the {uptime-guide}/uptime-security.html[Uptime Monitoring Guide]. + +[role="exclude",id="infra-read-only-access"] +== Configure source read-only access + +This page has moved. Please see <>. + diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 336adea0758abc..395f49358dc6b4 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -295,6 +295,10 @@ files that should be trusted. Details on the format, and the valid options, are available via the https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenSSL cipher list format documentation]. +`server.ssl.clientAuthentication:`:: *Default: none* Controls the server’s behavior in regard to requesting a certificate from client +connections. Valid values are `required`, `optional`, and `none`. `required` forces a client to present a certificate, while `optional` +requests a client certificate but the client is not required to present one. + `server.ssl.enabled:`:: *Default: "false"* Enables SSL for outgoing requests from the Kibana server to the browser. When set to `true`, `server.ssl.certificate` and `server.ssl.key` are required. diff --git a/docs/siem/index.asciidoc b/docs/siem/index.asciidoc index 6d8aea1f7fe63c..c947e000c81384 100644 --- a/docs/siem/index.asciidoc +++ b/docs/siem/index.asciidoc @@ -4,8 +4,6 @@ [partintro] -- -coming[7.2] - beta[] The SIEM app in Kibana provides an interactive workspace for security teams to diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index 4f0a1fc02adf11..210b044421e7e7 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -36,7 +36,6 @@ import { MapOfOptions, MapOfType, MaybeType, - NullableType, NeverType, NumberOptions, NumberType, @@ -102,7 +101,7 @@ function maybe(type: Type): Type { } function nullable(type: Type): Type { - return new NullableType(type); + return schema.oneOf([type, schema.literal(null)], { defaultValue: null }); } function object

(props: P, options?: ObjectTypeOptions

): ObjectType

{ diff --git a/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap index ae1a34c00d3a80..96ab664921fdfb 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap @@ -1,7 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`includes namespace in failure 1`] = `"[foo-namespace]: value is [foo] but it must have a maximum length of [1]."`; +exports[`includes namespace in failure 1`] = ` +"[foo-namespace]: types that failed validation: +- [foo-namespace.0]: value is [foo] but it must have a maximum length of [1]. +- [foo-namespace.1]: expected value to equal [null] but got [foo]" +`; -exports[`validates basic type 1`] = `"expected value of type [string] but got [number]"`; +exports[`validates basic type 1`] = ` +"types that failed validation: +- [0]: expected value of type [string] but got [number] +- [1]: expected value to equal [null] but got [666]" +`; -exports[`validates contained type 1`] = `"value is [foo] but it must have a maximum length of [1]."`; +exports[`validates contained type 1`] = ` +"types that failed validation: +- [0]: value is [foo] but it must have a maximum length of [1]. +- [1]: expected value to equal [null] but got [foo]" +`; + +exports[`validates type errors in object 1`] = ` +"[foo]: types that failed validation: +- [foo.0]: value is [ab] but it must have a maximum length of [1]. +- [foo.1]: expected value to equal [null] but got [ab]" +`; diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index 4c25cade2ec61d..cfa8cc4b7553d5 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -26,7 +26,6 @@ export { ConditionalType, ConditionalTypeValue } from './conditional_type'; export { DurationOptions, DurationType } from './duration_type'; export { LiteralType } from './literal_type'; export { MaybeType } from './maybe_type'; -export { NullableType } from './nullable_type'; export { MapOfOptions, MapOfType } from './map_type'; export { NumberOptions, NumberType } from './number_type'; export { ObjectType, ObjectTypeOptions, Props, TypeOf } from './object_type'; diff --git a/packages/kbn-config-schema/src/types/nullable_type.test.ts b/packages/kbn-config-schema/src/types/nullable_type.test.ts index e9d6f3ca2fe35e..ed04202950a2c9 100644 --- a/packages/kbn-config-schema/src/types/nullable_type.test.ts +++ b/packages/kbn-config-schema/src/types/nullable_type.test.ts @@ -19,19 +19,66 @@ import { schema } from '..'; -test('returns value if specified', () => { +test('returns string value when passed string', () => { const type = schema.nullable(schema.string()); - expect(type.validate('test')).toEqual('test'); + expect(type.validate('test')).toBe('test'); }); -test('returns null if null', () => { +test('returns number value when passed number', () => { + const type = schema.nullable(schema.number()); + expect(type.validate(42)).toBe(42); +}); + +test('returns boolean value when passed boolean', () => { + const type = schema.nullable(schema.boolean()); + expect(type.validate(true)).toBe(true); +}); + +test('returns object value when passed object', () => { + const type = schema.nullable( + schema.object({ + foo: schema.number(), + bar: schema.boolean(), + baz: schema.string(), + }) + ); + const object = { + foo: 666, + bar: true, + baz: 'foo bar baz', + }; + + expect(type.validate(object)).toEqual(object); +}); + +test('returns null if null for string', () => { const type = schema.nullable(schema.string()); - expect(type.validate(null)).toEqual(null); + expect(type.validate(null)).toBe(null); +}); + +test('returns null if null for number', () => { + const type = schema.nullable(schema.number()); + expect(type.validate(null)).toBe(null); }); -test('returns null if undefined', () => { +test('returns null if null for boolean', () => { + const type = schema.nullable(schema.boolean()); + expect(type.validate(null)).toBe(null); +}); + +test('returns null if undefined for string', () => { const type = schema.nullable(schema.string()); - expect(type.validate(undefined)).toEqual(null); + expect(type.validate(undefined)).toBe(null); +}); + +test('returns null if undefined for number', () => { + const type = schema.nullable(schema.number()); + expect(type.validate(undefined)).toBe(null); +}); + +test('returns null if undefined for boolean', () => { + const type = schema.nullable(schema.boolean()); + expect(type.validate(undefined)).toBe(null); }); test('returns null even if contained type has a default value', () => { @@ -41,7 +88,7 @@ test('returns null even if contained type has a default value', () => { }) ); - expect(type.validate(undefined)).toEqual(null); + expect(type.validate(undefined)).toBe(null); }); test('validates contained type', () => { @@ -56,6 +103,27 @@ test('validates basic type', () => { expect(() => type.validate(666)).toThrowErrorMatchingSnapshot(); }); +test('validates type in object', () => { + const type = schema.object({ + foo: schema.nullable(schema.string({ maxLength: 1 })), + bar: schema.nullable(schema.boolean()), + }); + + expect(type.validate({ foo: 'a' })).toEqual({ foo: 'a', bar: null }); + expect(type.validate({ foo: null })).toEqual({ foo: null, bar: null }); + expect(type.validate({})).toEqual({ foo: null, bar: null }); + expect(type.validate({ bar: null })).toEqual({ foo: null, bar: null }); +}); + +test('validates type errors in object', () => { + const type = schema.object({ + foo: schema.nullable(schema.string({ maxLength: 1 })), + bar: schema.nullable(schema.boolean()), + }); + + expect(() => type.validate({ foo: 'ab' })).toThrowErrorMatchingSnapshot(); +}); + test('includes namespace in failure', () => { const type = schema.nullable(schema.string({ maxLength: 1 })); diff --git a/packages/kbn-config-schema/src/types/nullable_type.ts b/packages/kbn-config-schema/src/types/nullable_type.ts deleted file mode 100644 index c89f3e44c37c4f..00000000000000 --- a/packages/kbn-config-schema/src/types/nullable_type.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Type } from './type'; - -export class NullableType extends Type { - constructor(type: Type) { - super( - type - .getSchema() - .optional() - .allow(null) - .default(null) - ); - } -} diff --git a/packages/kbn-plugin-helpers/lib/config_file.js b/packages/kbn-plugin-helpers/lib/config_file.js index fc359f838f67d6..5b8be836998e44 100644 --- a/packages/kbn-plugin-helpers/lib/config_file.js +++ b/packages/kbn-plugin-helpers/lib/config_file.js @@ -47,8 +47,9 @@ module.exports = function(root) { const deprecationMsg = 'has been removed from `@kbn/plugin-helpers`. ' + - 'During development your plugin must be located in `../kibana-extra/{pluginName}` ' + - 'relative to the Kibana directory to work with this package.\n'; + 'During development your plugin must live in `./plugins/{pluginName}` ' + + 'inside the Kibana folder or `../kibana-extra/{pluginName}` ' + + 'relative to the Kibana folder to work with this package.\n'; if (config.kibanaRoot) { throw new Error('The `kibanaRoot` config option ' + deprecationMsg); diff --git a/packages/kbn-plugin-helpers/lib/plugin_config.js b/packages/kbn-plugin-helpers/lib/plugin_config.js index 62a333728771f5..07bbbfedc15da7 100644 --- a/packages/kbn-plugin-helpers/lib/plugin_config.js +++ b/packages/kbn-plugin-helpers/lib/plugin_config.js @@ -24,7 +24,8 @@ const configFile = require('./config_file'); module.exports = function(root) { if (!root) root = process.cwd(); - const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'))); + const pluginPackageJsonPath = resolve(root, 'package.json'); + const pkg = JSON.parse(readFileSync(pluginPackageJsonPath)); const config = configFile(root); const buildSourcePatterns = [ @@ -35,10 +36,23 @@ module.exports = function(root) { '{lib,public,server,webpackShims,translations}/**/*', ]; + const kibanaExtraDir = resolve(root, '../../kibana'); + const kibanaPluginsDir = resolve(root, '../../'); + const isPluginOnKibanaExtra = pluginPackageJsonPath.includes(kibanaExtraDir); + const isPluginXpack = pkg.name === 'x-pack'; + + if (isPluginOnKibanaExtra && !isPluginXpack) { + console.warn( + `In the future we will disable ../kibana-extra/{pluginName}. You should move your plugin ${pkg.name} as soon as possible to ./plugins/{pluginName}` + ); + } + + const kibanaRootWhenNotXpackPlugin = isPluginOnKibanaExtra ? kibanaExtraDir : kibanaPluginsDir; + return Object.assign( { root: root, - kibanaRoot: pkg.name === 'x-pack' ? resolve(root, '..') : resolve(root, '../../kibana'), + kibanaRoot: isPluginXpack ? resolve(root, '..') : kibanaRootWhenNotXpackPlugin, serverTestPatterns: ['server/**/__tests__/**/*.js'], buildSourcePatterns: buildSourcePatterns, skipInstallDependencies: false, diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index bdb65809d811c6..57d9db5e8c1e4f 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -45,6 +45,7 @@ Object { "!SRP", "!CAMELLIA", ], + "clientAuthentication": "none", "enabled": false, "supportedProtocols": Array [ "TLSv1.1", diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 11eec99a6f0140..2b627c265dbba3 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -17,7 +17,9 @@ * under the License. */ -import { config } from '.'; +import { config, HttpConfig } from '.'; +import { Env } from '../config'; +import { getEnvOptions } from '../config/__mocks__/env'; test('has defaults for config', () => { const httpSchema = config.schema; @@ -111,6 +113,46 @@ describe('with TLS', () => { expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); }); + test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => { + const httpSchema = config.schema; + const obj = { + port: 1234, + ssl: { + enabled: false, + clientAuthentication: 'optional', + }, + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"[ssl]: must enable ssl to use [clientAuthentication]"` + ); + }); + + test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => { + const httpSchema = config.schema; + const obj = { + port: 1234, + ssl: { + enabled: false, + clientAuthentication: 'required', + }, + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"[ssl]: must enable ssl to use [clientAuthentication]"` + ); + }); + + test('can specify `none` for [clientAuthentication] if ssl is not enabled', () => { + const obj = { + ssl: { + enabled: false, + clientAuthentication: 'none', + }, + }; + + const configValue = config.schema.validate(obj); + expect(configValue.ssl.clientAuthentication).toBe('none'); + }); + test('can specify single `certificateAuthority` as a string', () => { const obj = { ssl: { @@ -202,4 +244,55 @@ describe('with TLS', () => { httpSchema.validate(allKnownWithOneUnknownProtocols) ).toThrowErrorMatchingSnapshot(); }); + + test('HttpConfig instance should properly interpret `none` client authentication', () => { + const httpConfig = new HttpConfig( + config.schema.validate({ + ssl: { + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'none', + }, + }), + Env.createDefault(getEnvOptions()) + ); + + expect(httpConfig.ssl.requestCert).toBe(false); + expect(httpConfig.ssl.rejectUnauthorized).toBe(false); + }); + + test('HttpConfig instance should properly interpret `optional` client authentication', () => { + const httpConfig = new HttpConfig( + config.schema.validate({ + ssl: { + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'optional', + }, + }), + Env.createDefault(getEnvOptions()) + ); + + expect(httpConfig.ssl.requestCert).toBe(true); + expect(httpConfig.ssl.rejectUnauthorized).toBe(false); + }); + + test('HttpConfig instance should properly interpret `required` client authentication', () => { + const httpConfig = new HttpConfig( + config.schema.validate({ + ssl: { + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'required', + }, + }), + Env.createDefault(getEnvOptions()) + ); + + expect(httpConfig.ssl.requestCert).toBe(true); + expect(httpConfig.ssl.rejectUnauthorized).toBe(true); + }); }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index f808a4e9032dda..a42d38fd4cb708 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -26,6 +26,8 @@ const validBasePathRegex = /(^$|^\/.*[^\/]$)/; const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; +// before update to make sure it's in sync with validation rules in Legacy +// https://github.com/elastic/kibana/blob/master/src/legacy/server/config/schema.js export const config = { path: 'server', schema: schema.object( diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index 7cda25d957b427..f31e5ffff23584 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -17,16 +17,22 @@ * under the License. */ +jest.mock('fs', () => ({ + readFileSync: jest.fn(), +})); + import supertest from 'supertest'; import { Request, ResponseToolkit } from 'hapi'; import Joi from 'joi'; -import { defaultValidationErrorHandler, HapiValidationError } from './http_tools'; +import { defaultValidationErrorHandler, HapiValidationError, getServerOptions } from './http_tools'; import { HttpServer } from './http_server'; -import { HttpConfig } from './http_config'; +import { HttpConfig, config } from './http_config'; import { Router } from './router'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { ByteSizeValue } from '@kbn/config-schema'; +import { Env } from '../config'; +import { getEnvOptions } from '../config/__mocks__/env'; const emptyOutput = { statusCode: 400, @@ -41,6 +47,8 @@ const emptyOutput = { }, }; +afterEach(() => jest.clearAllMocks()); + describe('defaultValidationErrorHandler', () => { it('formats value validation errors correctly', () => { expect.assertions(1); @@ -97,3 +105,68 @@ describe('timeouts', () => { await server.stop(); }); }); + +describe('getServerOptions', () => { + beforeEach(() => + jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`) + ); + + it('properly configures TLS with default options', () => { + const httpConfig = new HttpConfig( + config.schema.validate({ + ssl: { + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + }, + }), + Env.createDefault(getEnvOptions()) + ); + + expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "cert": "content-some-certificate-path", + "ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", + "honorCipherOrder": true, + "key": "content-some-key-path", + "passphrase": undefined, + "rejectUnauthorized": false, + "requestCert": false, + "secureOptions": 67108864, + } + `); + }); + + it('properly configures TLS with client authentication', () => { + const httpConfig = new HttpConfig( + config.schema.validate({ + ssl: { + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + certificateAuthorities: ['ca-1', 'ca-2'], + clientAuthentication: 'required', + }, + }), + Env.createDefault(getEnvOptions()) + ); + + expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(` + Object { + "ca": Array [ + "content-ca-1", + "content-ca-2", + ], + "cert": "content-some-certificate-path", + "ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", + "honorCipherOrder": true, + "key": "content-some-key-path", + "passphrase": undefined, + "rejectUnauthorized": true, + "requestCert": true, + "secureOptions": 67108864, + } + `); + }); +}); diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 2953d5272ebe93..88164a76c66f07 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -71,6 +71,7 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { passphrase: ssl.keyPassphrase, secureOptions: ssl.getSecureOptions(), requestCert: ssl.requestCert, + rejectUnauthorized: ssl.rejectUnauthorized, }; options.tls = tlsOptions; diff --git a/src/core/server/http/router/socket.test.ts b/src/core/server/http/router/socket.test.ts index 6bd903fd2f36ce..c813dcf3fc8063 100644 --- a/src/core/server/http/router/socket.test.ts +++ b/src/core/server/http/router/socket.test.ts @@ -56,4 +56,50 @@ describe('KibanaSocket', () => { expect(socket.getPeerCertificate()).toBe(null); }); }); + + describe('authorized', () => { + it('returns `undefined` for net.Socket instance', () => { + const socket = new KibanaSocket(new Socket()); + + expect(socket.authorized).toBeUndefined(); + }); + + it('mirrors the value of tls.Socket.authorized', () => { + const tlsSocket = new TLSSocket(new Socket()); + + tlsSocket.authorized = true; + let socket = new KibanaSocket(tlsSocket); + expect(tlsSocket.authorized).toBe(true); + expect(socket.authorized).toBe(true); + + tlsSocket.authorized = false; + socket = new KibanaSocket(tlsSocket); + expect(tlsSocket.authorized).toBe(false); + expect(socket.authorized).toBe(false); + }); + }); + + describe('authorizationError', () => { + it('returns `undefined` for net.Socket instance', () => { + const socket = new KibanaSocket(new Socket()); + + expect(socket.authorizationError).toBeUndefined(); + }); + + it('mirrors the value of tls.Socket.authorizationError', () => { + const tlsSocket = new TLSSocket(new Socket()); + tlsSocket.authorizationError = undefined as any; + + let socket = new KibanaSocket(tlsSocket); + expect(tlsSocket.authorizationError).toBeUndefined(); + expect(socket.authorizationError).toBeUndefined(); + + const authorizationError = new Error('some error'); + tlsSocket.authorizationError = authorizationError; + socket = new KibanaSocket(tlsSocket); + + expect(tlsSocket.authorizationError).toBe(authorizationError); + expect(socket.authorizationError).toBe(authorizationError); + }); + }); }); diff --git a/src/core/server/http/router/socket.ts b/src/core/server/http/router/socket.ts index 2cdcd8f6410015..83bf65a288c4b9 100644 --- a/src/core/server/http/router/socket.ts +++ b/src/core/server/http/router/socket.ts @@ -37,10 +37,30 @@ export interface IKibanaSocket { * @returns An object representing the peer's certificate. */ getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; + + /** + * Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS + * isn't used the value is `undefined`. + */ + readonly authorized?: boolean; + + /** + * The reason why the peer's certificate has not been verified. This property becomes available + * only when `authorized` is `false`. + */ + readonly authorizationError?: Error; } export class KibanaSocket implements IKibanaSocket { - constructor(private readonly socket: Socket) {} + readonly authorized?: boolean; + readonly authorizationError?: Error; + + constructor(private readonly socket: Socket) { + if (this.socket instanceof TLSSocket) { + this.authorized = this.socket.authorized; + this.authorizationError = this.socket.authorizationError; + } + } getPeerCertificate(detailed: true): DetailedPeerCertificate | null; getPeerCertificate(detailed: false): PeerCertificate | null; diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts index c32b94cf26defb..55d6ebff93ce78 100644 --- a/src/core/server/http/ssl_config.ts +++ b/src/core/server/http/ssl_config.ts @@ -49,13 +49,20 @@ export const sslSchema = schema.object( schema.oneOf([schema.literal('TLSv1'), schema.literal('TLSv1.1'), schema.literal('TLSv1.2')]), { defaultValue: ['TLSv1.1', 'TLSv1.2'], minSize: 1 } ), - requestCert: schema.maybe(schema.boolean({ defaultValue: false })), + clientAuthentication: schema.oneOf( + [schema.literal('none'), schema.literal('optional'), schema.literal('required')], + { defaultValue: 'none' } + ), }, { validate: ssl => { if (ssl.enabled && (!ssl.key || !ssl.certificate)) { return 'must specify [certificate] and [key] when ssl is enabled'; } + + if (!ssl.enabled && ssl.clientAuthentication !== 'none') { + return 'must enable ssl to use [clientAuthentication]'; + } }, } ); @@ -69,7 +76,8 @@ export class SslConfig { public certificate: string | undefined; public certificateAuthorities: string[] | undefined; public keyPassphrase: string | undefined; - public requestCert: boolean | undefined; + public requestCert: boolean; + public rejectUnauthorized: boolean; public cipherSuites: string[]; public supportedProtocols: string[]; @@ -86,7 +94,8 @@ export class SslConfig { this.keyPassphrase = config.keyPassphrase; this.cipherSuites = config.cipherSuites; this.supportedProtocols = config.supportedProtocols; - this.requestCert = config.requestCert; + this.requestCert = config.clientAuthentication !== 'none'; + this.rejectUnauthorized = config.clientAuthentication === 'required'; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 6597fdf0259d40..49188fbd7b6636 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -261,6 +261,8 @@ export type IContextProvider, TContextName // @public export interface IKibanaSocket { + readonly authorizationError?: Error; + readonly authorized?: boolean; // (undocumented) getPeerCertificate(detailed: true): DetailedPeerCertificate | null; // (undocumented) diff --git a/src/dev/build/tasks/nodejs_modules/webpack_dll.js b/src/dev/build/tasks/nodejs_modules/webpack_dll.js index 07ca8230a1585f..e741784e7f866d 100644 --- a/src/dev/build/tasks/nodejs_modules/webpack_dll.js +++ b/src/dev/build/tasks/nodejs_modules/webpack_dll.js @@ -18,9 +18,10 @@ */ import { deleteAll, isFileAccessible, read, write } from '../../lib'; -import { dirname, sep, relative, resolve } from 'path'; +import { dirname, relative, resolve } from 'path'; import pkgUp from 'pkg-up'; import globby from 'globby'; +import normalizePosixPath from 'normalize-path'; function checkDllEntryAccess(entry, baseDir = '') { const resolvedPath = baseDir ? resolve(baseDir, entry) : entry; @@ -48,7 +49,7 @@ export async function getDllEntries(manifestPath, whiteListedModules, baseDir = // Only includes modules who are not in the white list of modules // and that are node_modules return modules.filter(entry => { - const isWhiteListed = whiteListedModules.some(nonEntry => entry.includes(`node_modules${sep}${nonEntry}${sep}`)); + const isWhiteListed = whiteListedModules.some(nonEntry => normalizePosixPath(entry).includes(`node_modules/${nonEntry}`)); const isNodeModule = entry.includes('node_modules'); // NOTE: when using dynamic imports on webpack the entry paths could be created @@ -69,6 +70,7 @@ export async function cleanDllModuleFromEntryPath(logger, entryPath) { const modulePkgPath = await pkgUp(entryPath); const modulePkg = JSON.parse(await read(modulePkgPath)); const moduleDir = dirname(modulePkgPath); + const normalizedModuleDir = normalizePosixPath(moduleDir); // Cancel the cleanup for this module as it // was already done. @@ -93,9 +95,9 @@ export async function cleanDllModuleFromEntryPath(logger, entryPath) { // until the following issue gets closed // https://github.com/sindresorhus/globby/issues/87 const filesToDelete = await globby([ - `${moduleDir}/**`, - `!${moduleDir}/**/*.+(css)`, - `!${moduleDir}/**/*.+(gif|ico|jpeg|jpg|tiff|tif|svg|png|webp)`, + `${normalizedModuleDir}/**`, + `!${normalizedModuleDir}/**/*.+(css)`, + `!${normalizedModuleDir}/**/*.+(gif|ico|jpeg|jpg|tiff|tif|svg|png|webp)`, ]); await deleteAll(filesToDelete.filter(path => { diff --git a/src/dev/ci_setup/get_percy_env.js b/src/dev/ci_setup/get_percy_env.js index ead64fd2b90567..66853ef2a5afcd 100644 --- a/src/dev/ci_setup/get_percy_env.js +++ b/src/dev/ci_setup/get_percy_env.js @@ -23,7 +23,36 @@ const pkg = require('../../../package.json'); const { stdout: commit } = execa.sync('git', ['rev-parse', 'HEAD']); const shortCommit = commit.slice(0, 8); +if (!process.env.JOB_NAME) { + throw new Error('getPercyEnv: [JOB_NAME] environment variable required'); +} + const isPr = process.env.JOB_NAME.includes('elastic+kibana+pull-request'); +if (isPr && !(process.env.PR_TARGET_BRANCH && process.env.PR_SOURCE_BRANCH)) { + throw new Error( + 'getPercyEnv: Unable to determine percy environment in prs without [PR_TARGET_BRANCH] and [PR_SOURCE_BRANCH] environment variables' + ); +} + +let branch; +if (isPr) { + branch = process.env.PR_SOURCE_BRANCH; +} else { + if (!process.env.branch_specifier) { + throw new Error('getPercyEnv: [branch_specifier] environment variable required'); + } + + branch = process.env.branch_specifier.split('refs/heads/')[1]; + + if (!branch) { + throw new Error( + `getPercyEnv: [branch_specifier=${process.env.branch_specifier}] must start with 'refs/heads/'` + ); + } +} console.log(`export PERCY_PARALLEL_TOTAL=2;`); -console.log(`export PERCY_PARALLEL_NONCE="${shortCommit}/${isPr ? 'PR' : pkg.branch}/${process.env.BUILD_ID}";`); +console.log(`export PERCY_PARALLEL_NONCE="${shortCommit}/${isPr ? 'PR' : branch}/${process.env.BUILD_ID}";`); +console.log(`export PERCY_BRANCH="${branch}";`); +// percy snapshots always target pkg.branch, so that feature branches can be based on master/7.x/etc. +console.log(`export PERCY_TARGET_BRANCH="${isPr ? process.env.PR_TARGET_BRANCH : pkg.branch}";`); diff --git a/src/dev/ci_setup/setup_percy.sh b/src/dev/ci_setup/setup_percy.sh index c008dc1d5cdbdc..ed6a15d056fe5a 100755 --- a/src/dev/ci_setup/setup_percy.sh +++ b/src/dev/ci_setup/setup_percy.sh @@ -15,3 +15,5 @@ export PUPPETEER_EXECUTABLE_PATH eval "$(node ./src/dev/ci_setup/get_percy_env)" echo " -- PERCY_PARALLEL_NONCE='$PERCY_PARALLEL_NONCE'" echo " -- PERCY_PARALLEL_TOTAL='$PERCY_PARALLEL_TOTAL'" +echo " -- PERCY_BRANCH='$PERCY_BRANCH'" +echo " -- PERCY_TARGET_BRANCH='$PERCY_TARGET_BRANCH'" diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/tests/dashboard_container.test.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/tests/dashboard_container.test.tsx index b70da90b046c41..3c33b0bde401cf 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/tests/dashboard_container.test.tsx +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/tests/dashboard_container.test.tsx @@ -70,7 +70,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { []) as any} + getActions={() => Promise.resolve([])} getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => null) as any} notifications={{} as any} diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.js b/src/legacy/core_plugins/elasticsearch/lib/abortable_request_handler.js similarity index 62% rename from src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.js rename to src/legacy/core_plugins/elasticsearch/lib/abortable_request_handler.js index c0635d949115d1..0b8786f0c28413 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.js +++ b/src/legacy/core_plugins/elasticsearch/lib/abortable_request_handler.js @@ -17,18 +17,18 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -import vislibGridTemplate from './grid.html'; -const module = uiModules.get('kibana'); +import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -module.directive('vislibGrid', function () { - return { - restrict: 'E', - template: vislibGridTemplate, - replace: true, - link: function ($scope) { - - $scope.isGridOpen = true; - } +/* + * A simple utility for generating a handler that provides a signal to the handler that signals when + * the client has closed the connection on this request. + */ +export function abortableRequestHandler(fn) { + return (req, ...args) => { + const controller = new AbortController(); + req.events.once('disconnect', () => { + controller.abort(); + }); + return fn(controller.signal, req, ...args); }; -}); +} diff --git a/src/legacy/core_plugins/elasticsearch/lib/abortable_request_handler.test.js b/src/legacy/core_plugins/elasticsearch/lib/abortable_request_handler.test.js new file mode 100644 index 00000000000000..09bf701ce3723f --- /dev/null +++ b/src/legacy/core_plugins/elasticsearch/lib/abortable_request_handler.test.js @@ -0,0 +1,58 @@ +/* + * 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 { AbortSignal } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; +import { abortableRequestHandler } from './abortable_request_handler'; + +describe('abortableRequestHandler', () => { + jest.useFakeTimers(); + + it('should call abort if disconnected', () => { + const eventHandlers = new Map(); + const mockRequest = { + events: { + once: jest.fn((key, fn) => eventHandlers.set(key, fn)) + } + }; + + const handler = jest.fn(); + const onAborted = jest.fn(); + const abortableHandler = abortableRequestHandler(handler); + abortableHandler(mockRequest); + + const [signal, request] = handler.mock.calls[0]; + + expect(signal instanceof AbortSignal).toBe(true); + expect(request).toBe(mockRequest); + + signal.addEventListener('abort', onAborted); + + // Shouldn't be aborted or call onAborted prior to disconnecting + expect(signal.aborted).toBe(false); + expect(onAborted).not.toBeCalled(); + + expect(eventHandlers.has('disconnect')).toBe(true); + eventHandlers.get('disconnect')(); + jest.runAllTimers(); + + // Should be aborted and call onAborted after disconnecting + expect(signal.aborted).toBe(true); + expect(onAborted).toBeCalled(); + }); +}); diff --git a/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js b/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js index d1ad422b5c675f..0547cedf93e9d3 100644 --- a/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js +++ b/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @@ -18,6 +18,7 @@ */ import Joi from 'joi'; +import { abortableRequestHandler } from './abortable_request_handler'; export function createProxy(server) { const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); @@ -30,15 +31,15 @@ export function createProxy(server) { parse: 'gunzip' } }, - async handler(req, h) { + handler: abortableRequestHandler((signal, req, h) => { const { query, payload } = req; return callWithRequest(req, 'transport.request', { path: '/_msearch', method: 'POST', query, body: payload.toString('utf8') - }).finally(r => h.response(r)); - } + }, { signal }).finally(r => h.response(r)); + }) }); server.route({ @@ -51,14 +52,14 @@ export function createProxy(server) { }) } }, - handler(req, h) { + handler: abortableRequestHandler((signal, req, h) => { const { query, payload: body } = req; return callWithRequest(req, 'transport.request', { path: `/${encodeURIComponent(req.params.index)}/_search`, method: 'POST', query, body - }).finally(r => h.response(r)); - } + }, { signal }).finally(r => h.response(r)); + }) }); } diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/bootstrap.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/bootstrap.ts index 5249c169798d82..2ccc0bdb5f7af5 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/bootstrap.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/bootstrap.ts @@ -18,7 +18,12 @@ */ import { EmbeddableApi } from './api/types'; -import { CONTEXT_MENU_TRIGGER, APPLY_FILTER_TRIGGER, ApplyFilterAction } from './lib'; +import { + CONTEXT_MENU_TRIGGER, + APPLY_FILTER_TRIGGER, + ApplyFilterAction, + PANEL_BADGE_TRIGGER, +} from './lib'; /** * This method initializes Embeddable plugin with initial set of @@ -39,10 +44,17 @@ export const bootstrap = (api: EmbeddableApi) => { description: 'Triggered when user applies filter to an embeddable.', actionIds: [], }; + const triggerBadge = { + id: PANEL_BADGE_TRIGGER, + title: 'Panel badges', + description: 'Actions appear in title bar when an embeddable loads in a panel', + actionIds: [], + }; const actionApplyFilter = new ApplyFilterAction(); api.registerTrigger(triggerContext); api.registerTrigger(triggerFilter); api.registerAction(actionApplyFilter); + api.registerTrigger(triggerBadge); api.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts index 31c0cddb011a07..ff264f72caccae 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts @@ -24,6 +24,7 @@ export { ADD_PANEL_ACTION_ID, APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, + PANEL_BADGE_TRIGGER, Action, ActionContext, Adapters, diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/container.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/container.ts index 71749fd00d8dd9..f4fca4bbd8d6e1 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/container.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/container.ts @@ -35,10 +35,10 @@ const getKeys = (o: T): Array => Object.keys(o) as Array< export abstract class Container< TChildInput extends Partial = {}, - TContainerInput extends ContainerInput = ContainerInput, + TContainerInput extends ContainerInput = ContainerInput, TContainerOutput extends ContainerOutput = ContainerOutput > extends Embeddable - implements IContainer { + implements IContainer { public readonly isContainer: boolean = true; protected readonly children: { [key: string]: IEmbeddable | ErrorEmbeddable; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/embeddable_child_panel.test.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/embeddable_child_panel.test.tsx index 307699d35f5d73..2335173c4ef349 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/embeddable_child_panel.test.tsx @@ -63,7 +63,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async intl={null as any} container={container} embeddableId={newEmbeddable.id} - getActions={(() => undefined) as any} + getActions={() => Promise.resolve([])} getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => undefined) as any} notifications={{} as any} @@ -102,7 +102,7 @@ test(`EmbeddableChildPanel renders an error message if the factory doesn't exist intl={null as any} container={container} embeddableId={'1'} - getActions={(() => undefined) as any} + getActions={() => Promise.resolve([])} getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => undefined) as any} notifications={{} as any} diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/i_container.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/i_container.ts index 02c1401ceb4629..8f094447064f0b 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/i_container.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/containers/i_container.ts @@ -50,7 +50,8 @@ export interface ContainerInput extends EmbeddableInput } export interface IContainer< - I extends ContainerInput = ContainerInput, + Inherited extends {} = {}, + I extends ContainerInput = ContainerInput, O extends ContainerOutput = ContainerOutput > extends IEmbeddable { /** diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.test.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.test.tsx index 0e3cfeb2acbbb8..72e26434df0950 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.test.tsx @@ -156,7 +156,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => { undefined) as any} + getActions={() => Promise.resolve([])} getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => undefined) as any} notifications={{} as any} @@ -193,7 +193,7 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => { undefined) as any} + getActions={() => Promise.resolve([])} getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => undefined) as any} notifications={{} as any} @@ -255,7 +255,7 @@ test('Updates when hidePanelTitles is toggled', async () => { undefined) as any} + getActions={() => Promise.resolve([])} getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => undefined) as any} notifications={{} as any} diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx index 0a3d0a2bdf7c2b..c06f4ed99e8fd0 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx @@ -23,7 +23,7 @@ import { Subscription } from 'rxjs'; import { CoreStart } from '../../../../../../../../core/public'; import { buildContextMenuForActions } from '../context_menu_actions'; -import { CONTEXT_MENU_TRIGGER } from '../triggers'; +import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER } from '../triggers'; import { IEmbeddable } from '../embeddables/i_embeddable'; import { ViewMode, @@ -58,6 +58,7 @@ interface State { viewMode: ViewMode; hidePanelTitles: boolean; closeContextMenu: boolean; + badges: Action[]; } export class EmbeddablePanel extends React.Component { @@ -80,11 +81,24 @@ export class EmbeddablePanel extends React.Component { viewMode, hidePanelTitles, closeContextMenu: false, + badges: [], }; this.embeddableRoot = React.createRef(); } + private async refreshBadges() { + const badges = await this.props.getActions(PANEL_BADGE_TRIGGER, { + embeddable: this.props.embeddable, + }); + + if (this.mounted) { + this.setState({ + badges, + }); + } + } + public componentWillMount() { this.mounted = true; const { embeddable } = this.props; @@ -95,6 +109,8 @@ export class EmbeddablePanel extends React.Component { this.setState({ viewMode: embeddable.getInput().viewMode ? embeddable.getInput().viewMode : ViewMode.EDIT, }); + + this.refreshBadges(); } }); @@ -104,6 +120,8 @@ export class EmbeddablePanel extends React.Component { this.setState({ hidePanelTitles: Boolean(parent.getInput().hidePanelTitles), }); + + this.refreshBadges(); } }); } @@ -144,6 +162,8 @@ export class EmbeddablePanel extends React.Component { isViewMode={viewOnlyMode} closeContextMenu={this.state.closeContextMenu} title={title} + badges={this.state.badges} + embeddable={this.props.embeddable} />

diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts index fbea36e35f53d7..f8fc9d02992d46 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts @@ -29,9 +29,9 @@ interface ExpandedPanelInput extends ContainerInput { } function hasExpandedPanelInput( - container: IContainer | IContainer -): container is IContainer { - return (container as IContainer).getInput().expandedPanelId !== undefined; + container: IContainer +): container is IContainer<{}, ExpandedPanelInput> { + return (container as IContainer<{}, ExpandedPanelInput>).getInput().expandedPanelId !== undefined; } export class RemovePanelAction extends Action { diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_header.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_header.tsx index 3d6f82b94693db..92597e3a8b0e27 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_header.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_header.tsx @@ -17,11 +17,13 @@ * under the License. */ -import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { EuiContextMenuPanelDescriptor, EuiBadge } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React from 'react'; import { PanelOptionsMenu } from './panel_options_menu'; +import { Action } from '../../actions'; +import { IEmbeddable } from '../../embeddables'; export interface PanelHeaderProps { title?: string; @@ -29,12 +31,27 @@ export interface PanelHeaderProps { hidePanelTitles: boolean; getActionContextMenuPanel: () => Promise; closeContextMenu: boolean; + badges: Action[]; + embeddable: IEmbeddable; } interface PanelHeaderUiProps extends PanelHeaderProps { intl: InjectedIntl; } +function renderBadges(badges: Action[], embeddable: IEmbeddable) { + return badges.map(badge => ( + badge.execute({ embeddable })} + onClickAriaLabel={badge.getDisplayName({ embeddable })} + > + {badge.getDisplayName({ embeddable })} + + )); +} + function PanelHeaderUi({ title, isViewMode, @@ -42,12 +59,17 @@ function PanelHeaderUi({ getActionContextMenuPanel, intl, closeContextMenu, + badges, + embeddable, }: PanelHeaderUiProps) { const classes = classNames('embPanel__header', { 'embPanel__header--floater': !title || hidePanelTitles, }); - if (isViewMode && (!title || hidePanelTitles)) { + const showTitle = !isViewMode || (title && !hidePanelTitles); + const showPanelBar = badges.length > 0 || showTitle; + + if (!showPanelBar) { return (
- {hidePanelTitles ? '' : title} + {showTitle ? `${title} ` : ''} + {renderBadges(badges, embeddable)}
' + '
' }, - { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + { name: 'options', title: 'Panel Settings', editor: PointSeriesOptions }, ], schemas: new Schemas([ { diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.html b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.html deleted file mode 100644 index 73832f7b741bbd..00000000000000 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.html +++ /dev/null @@ -1,59 +0,0 @@ -
-
-
- - - - -
-
- -
- -
- -
- -
-
- -
- -
- -
-
-
-
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid_options.tsx new file mode 100644 index 00000000000000..069a2a0dcbeab9 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid_options.tsx @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useMemo, useEffect } from 'react'; +import { EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisOptionsProps } from 'ui/vis/editors/default'; +import { SwitchOption } from '../switch'; +import { SelectOption } from '../select'; +import { BasicVislibParams, ValueAxis } from '../../types'; + +function GridOptions({ + stateParams, + setValue, + hasHistogramAgg, +}: VisOptionsProps) { + const setGrid = ( + paramName: T, + value: BasicVislibParams['grid'][T] + ) => setValue('grid', { ...stateParams.grid, [paramName]: value }); + + const options = useMemo( + () => [ + ...stateParams.valueAxes.map(({ id, name }: ValueAxis) => ({ + text: name, + value: id, + })), + { + text: i18n.translate('kbnVislibVisTypes.controls.pointSeries.gridAxis.dontShowLabel', { + defaultMessage: "Don't show", + }), + value: '', + }, + ], + [stateParams.valueAxes.map(({ id }: ValueAxis) => id)] + ); + + useEffect(() => { + if (hasHistogramAgg) { + setGrid('categoryLines', false); + } + }, [hasHistogramAgg]); + + return ( + + +

+ +

+
+ + + + +
+ ); +} + +export { GridOptions }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js index 599a99b9214864..87289e9aa410d8 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js @@ -20,4 +20,3 @@ import './value_axes.js'; import './category_axis.js'; import './series.js'; -import './grid.js'; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/select.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/select.tsx index 01c6a576d4d1b3..0a0aeeb26dcef8 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/select.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/select.tsx @@ -21,14 +21,17 @@ import React from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; interface SelectOptionProps { + id?: string; label: string; options: Array<{ value: ValidParamValues; text: string }>; paramName: ParamName; value?: ValidParamValues; + dataTestSubj?: string; setValue: (paramName: ParamName, value: ValidParamValues) => void; } function SelectOption({ + id, label, options, paramName, @@ -36,7 +39,7 @@ function SelectOption setValue, }: SelectOptionProps) { return ( - + - -
-
-
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- - -
- - \ No newline at end of file diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/point_series.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/point_series.tsx new file mode 100644 index 00000000000000..fcff704878fedd --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/point_series.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisOptionsProps } from 'ui/vis/editors/default'; +import { SwitchOption } from '../controls/switch'; +import { GridOptions } from '../controls/point_series/grid_options'; +import { BasicOptions } from '../controls/basic_options'; +import { BasicVislibParams } from '../types'; + +function PointSeriesOptions(props: VisOptionsProps) { + const { stateParams, setValue, vis } = props; + + return ( + <> + + +

+ +

+
+ + + + + {vis.hasSchemaAgg('segment', 'date_histogram') ? ( + + ) : ( + + )} + + {vis.type.type === 'histogram' && ( + + setValue('labels', { ...stateParams.labels, [paramName]: value }) + } + /> + )} +
+ + + + + + ); +} + +export { PointSeriesOptions }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js index 07ddbcb941fdc0..4c699ab22ed623 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js @@ -20,7 +20,8 @@ import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; -import pointSeriesTemplate from './editors/point_series.html'; +import { PointSeriesOptions } from './editors/point_series'; +import { getLegendPositions, LegendPositions } from './utils/legend_positions'; export default function PointSeriesVisType(Private) { const VisFactory = Private(VisFactoryProvider); @@ -95,7 +96,7 @@ export default function PointSeriesVisType(Private) { ], addTooltip: true, addLegend: true, - legendPosition: 'right', + legendPosition: LegendPositions.RIGHT, times: [], addTimeMarker: false, labels: { @@ -105,6 +106,7 @@ export default function PointSeriesVisType(Private) { }, editorConfig: { collections: { + legendPositions: getLegendPositions(), positions: ['top', 'left', 'right', 'bottom'], chartTypes: [{ value: 'line', @@ -137,7 +139,7 @@ export default function PointSeriesVisType(Private) { editor: '
' + '
' }, - { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + { name: 'options', title: 'Panel Settings', editor: PointSeriesOptions }, ], schemas: new Schemas([ { diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js index 5707b32b2c395f..c3f0b6ab0c8a9f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js @@ -20,7 +20,8 @@ import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; -import pointSeriesTemplate from './editors/point_series.html'; +import { PointSeriesOptions } from './editors/point_series'; +import { getLegendPositions, LegendPositions } from './utils/legend_positions'; export default function PointSeriesVisType(Private) { const VisFactory = Private(VisFactoryProvider); @@ -96,13 +97,15 @@ export default function PointSeriesVisType(Private) { }], addTooltip: true, addLegend: true, - legendPosition: 'right', + legendPosition: LegendPositions.RIGHT, times: [], addTimeMarker: false, + labels: {}, }, }, editorConfig: { collections: { + legendPositions: getLegendPositions(), positions: ['top', 'left', 'right', 'bottom'], chartTypes: [{ value: 'line', @@ -135,7 +138,7 @@ export default function PointSeriesVisType(Private) { editor: '
' + '
' }, - { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + { name: 'options', title: 'Panel Settings', editor: PointSeriesOptions }, ], schemas: new Schemas([ { diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js index e7c84fb6fc1ce1..5261b757a78206 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js @@ -20,7 +20,8 @@ import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; -import pointSeriesTemplate from './editors/point_series.html'; +import { PointSeriesOptions } from './editors/point_series'; +import { getLegendPositions, LegendPositions } from './utils/legend_positions'; export default function PointSeriesVisType(Private) { const VisFactory = Private(VisFactoryProvider); @@ -93,13 +94,15 @@ export default function PointSeriesVisType(Private) { ], addTooltip: true, addLegend: true, - legendPosition: 'right', + legendPosition: LegendPositions.RIGHT, times: [], addTimeMarker: false, + labels: {}, }, }, editorConfig: { collections: { + legendPositions: getLegendPositions(), positions: ['top', 'left', 'right', 'bottom'], chartTypes: [{ value: 'line', @@ -132,7 +135,7 @@ export default function PointSeriesVisType(Private) { editor: '
' + '
' }, - { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + { name: 'options', title: 'Panel Settings', editor: PointSeriesOptions }, ], schemas: new Schemas([ { diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/types.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/types.ts index 8bf941d8b94586..05d049d1c9e1e5 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/types.ts +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/types.ts @@ -17,7 +17,38 @@ * under the License. */ +import { LegendPositions } from './utils/legend_positions'; + export interface CommonVislibParams { addTooltip: boolean; - legendPosition: 'right' | 'left' | 'top' | 'bottom'; + legendPosition: LegendPositions; +} + +interface Labels { + filter: boolean; + rotate?: number; + show: boolean; + truncate: number; +} +export interface ValueAxis { + id: string; + labels: Labels; + name: string; + position: LegendPositions; + scale: { type: string }; + show: boolean; + style: object; + title: { text: string }; + type: string; +} + +export interface BasicVislibParams extends CommonVislibParams { + addTimeMarker: boolean; + orderBucketsBySum?: boolean; + labels: Labels; + valueAxes: ValueAxis[]; + grid: { + categoryLines: boolean; + valueAxis?: string; + }; } diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/legend_positions.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/legend_positions.ts new file mode 100644 index 00000000000000..ffe70eba19b394 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/legend_positions.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 { i18n } from '@kbn/i18n'; + +export enum LegendPositions { + RIGHT = 'right', + LEFT = 'left', + TOP = 'top', + BOTTOM = 'bottom', +} + +const getLegendPositions = () => [ + { + text: i18n.translate('kbnVislibVisTypes.legendPositions.topText', { + defaultMessage: 'Top', + }), + value: LegendPositions.TOP, + }, + { + text: i18n.translate('kbnVislibVisTypes.legendPositions.leftText', { + defaultMessage: 'Left', + }), + value: LegendPositions.LEFT, + }, + { + text: i18n.translate('kbnVislibVisTypes.legendPositions.rightText', { + defaultMessage: 'Right', + }), + value: LegendPositions.RIGHT, + }, + { + text: i18n.translate('kbnVislibVisTypes.legendPositions.bottomText', { + defaultMessage: 'Bottom', + }), + value: LegendPositions.BOTTOM, + }, +]; + +export { getLegendPositions }; diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js index 40e3e8a5e88d0b..bd4340dbadb0fb 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.js @@ -365,6 +365,24 @@ function replaceMovAvgToMovFn(doc, logger) { return doc; } +function migrateSearchSortToNestedArray(doc) { + const sort = get(doc, 'attributes.sort'); + if (!sort) return doc; + + // Don't do anything if we already have a two dimensional array + if (Array.isArray(sort) && sort.length > 0 && Array.isArray(sort[0])) { + return doc; + } + + return { + ...doc, + attributes: { + ...doc.attributes, + sort: [doc.attributes.sort], + } + }; +} + const executeMigrations720 = flow( migratePercentileRankAggregation, migrateDateHistogramAggregation @@ -376,6 +394,10 @@ const executeMigrations730 = flow( replaceMovAvgToMovFn ); +const executeSearchMigrations740 = flow( + migrateSearchSortToNestedArray, +); + export const migrations = { 'index-pattern': { '6.5.0': doc => { @@ -530,5 +552,6 @@ export const migrations = { migrateIndexPattern(doc); return doc; }, + '7.4.0': executeSearchMigrations740, }, }; diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.test.js b/src/legacy/core_plugins/kibana/migrations/migrations.test.js index 37a9006f9eeb93..0776b6028641f3 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.test.js @@ -1788,4 +1788,56 @@ Object { /* eslint-enable max-len */ }); }); + + describe('7.4.0', function () { + const migration = migrations.search['7.4.0']; + + test('transforms one dimensional sort arrays into two dimensional arrays', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + sort: ['bytes', 'desc'], + }, + }; + + const expected = { + id: '123', + type: 'search', + attributes: { + sort: [['bytes', 'desc']], + }, + }; + + const migratedDoc = migration(doc); + + expect(migratedDoc).toEqual(expected); + }); + + test('doesn\'t modify search docs that already have two dimensional sort arrays', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + sort: [['bytes', 'desc']], + }, + }; + + const migratedDoc = migration(doc); + + expect(migratedDoc).toEqual(doc); + }); + + test('doesn\'t modify search docs that have no sort array', () => { + const doc = { + id: '123', + type: 'search', + attributes: {}, + }; + + const migratedDoc = migration(doc); + + expect(migratedDoc).toEqual(doc); + }); + }); }); diff --git a/src/legacy/core_plugins/kibana/public/context/query/actions.js b/src/legacy/core_plugins/kibana/public/context/query/actions.js index b61a1f8f687eea..142226e5aa98c8 100644 --- a/src/legacy/core_plugins/kibana/public/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/context/query/actions.js @@ -20,13 +20,13 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { MarkdownSimple } from 'ui/markdown'; import { toastNotifications } from 'ui/notify'; import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; import { QueryParameterActionsProvider } from '../query_parameters'; import { FAILURE_REASONS, LOADING_STATUS } from './constants'; +import { MarkdownSimple } from '../../../../kibana_react/public'; export function QueryActionsProvider(Private, Promise) { const fetchAnchor = Private(fetchAnchorProvider); diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 290b92a395e00c..18355281c59618 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -551,6 +551,7 @@ function discoverController( $scope.$listen(timefilter, 'autoRefreshFetch', $scope.fetch); $scope.$listen(timefilter, 'refreshIntervalUpdate', $scope.updateRefreshInterval); $scope.$listen(timefilter, 'timeUpdate', $scope.updateTime); + $scope.$listen(timefilter, 'fetch', $scope.fetch); $scope.$watchCollection('state.sort', function (sort) { if (!sort) return; diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js index b8b962b9f92d71..18d4d1298d83de 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js @@ -38,11 +38,15 @@ describe('docTable', function () { expect(getSort).to.be.a(Function); }); - it('should return an array of objects if passed a 2 item array', function () { - expect(getSort(['bytes', 'desc'], indexPattern)).to.eql([{ bytes: 'desc' }]); + it('should return an array of objects', function () { + expect(getSort([['bytes', 'desc']], indexPattern)).to.eql([{ bytes: 'desc' }]); delete indexPattern.timeFieldName; - expect(getSort(['bytes', 'desc'], indexPattern)).to.eql([{ bytes: 'desc' }]); + expect(getSort([['bytes', 'desc']], indexPattern)).to.eql([{ bytes: 'desc' }]); + }); + + it('should passthrough arrays of objects', () => { + expect(getSort([{ bytes: 'desc' }], indexPattern)).to.eql([{ bytes: 'desc' }]); }); it('should sort by the default when passed an unsortable field', function () { @@ -74,7 +78,26 @@ describe('docTable', function () { }); it('should return an array of arrays for sortable fields', function () { - expect(getSort.array(['bytes', 'desc'], indexPattern)).to.eql([[ 'bytes', 'desc' ]]); + expect(getSort.array([['bytes', 'desc']], indexPattern)).to.eql([[ 'bytes', 'desc' ]]); + }); + + it('should return an array of arrays from an array of elasticsearch sort objects', function () { + expect(getSort.array([{ bytes: 'desc' }], indexPattern)).to.eql([[ 'bytes', 'desc' ]]); + }); + + it('should sort by the default when passed an unsortable field', function () { + expect(getSort.array([{ 'non-sortable': 'asc' }], indexPattern)).to.eql([['time', 'desc']]); + expect(getSort.array([{ lol_nope: 'asc' }], indexPattern)).to.eql([['time', 'desc']]); + + delete indexPattern.timeFieldName; + expect(getSort.array([{ 'non-sortable': 'asc' }], indexPattern)).to.eql([[ '_score', 'desc' ]]); + }); + + it('should sort by the default when passed an empty sort', () => { + expect(getSort.array([], indexPattern)).to.eql([['time', 'desc']]); + + delete indexPattern.timeFieldName; + expect(getSort.array([], indexPattern)).to.eql([[ '_score', 'desc' ]]); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js index 9aba887569dbf6..0af57c158e21ab 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/lib/get_sort.js @@ -29,6 +29,9 @@ function createSortObject(sortPair, indexPattern) { const [ field, direction ] = sortPair; return { [field]: direction }; } + else if (_.isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], indexPattern)) { + return sortPair; + } else { return undefined; } @@ -36,7 +39,8 @@ function createSortObject(sortPair, indexPattern) { /** * Take a sorting array and make it into an object - * @param {array} sort 2 item array [fieldToSort, directionToSort] + * @param {array} sort two dimensional array [[fieldToSort, directionToSort]] + * or an array of objects [{fieldToSort: directionToSort}] * @param {object} indexPattern used for determining default sort * @returns {object} a sort object suitable for returning to elasticsearch */ @@ -44,9 +48,6 @@ export function getSort(sort, indexPattern, defaultSortOrder = 'desc') { let sortObjects; if (Array.isArray(sort)) { - if (sort.length > 0 && !Array.isArray(sort[0])) { - sort = [sort]; - } sortObjects = _.compact(sort.map((sortPair) => createSortObject(sortPair, indexPattern))); } diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index 576abc5427a929..8288e3d1d1d7a6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -45,11 +45,11 @@ import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; interface SearchScope extends ng.IScope { columns?: string[]; description?: string; - sort?: string[] | string[][]; + sort?: string[][]; searchSource?: SearchSource; sharedItemTitle?: string; inspectorAdapters?: Adapters; - setSortOrder?: (sortPair: [string, string]) => void; + setSortOrder?: (sortPair: [[string, string]]) => void; removeColumn?: (column: string) => void; addColumn?: (column: string) => void; moveColumn?: (column: string, index: number) => void; @@ -264,9 +264,6 @@ export class SearchEmbeddable extends Embeddable // been overridden in a dashboard. searchScope.columns = this.input.columns || this.savedSearch.columns; searchScope.sort = this.input.sort || this.savedSearch.sort; - if (searchScope.sort.length && !Array.isArray(searchScope.sort[0])) { - searchScope.sort = [searchScope.sort]; - } searchScope.sharedItemTitle = this.panelTitle; if ( diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts index 5791216acde41c..43648d575014ee 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts @@ -34,7 +34,7 @@ export interface SearchInput extends EmbeddableInput { filters?: Filter[]; hidePanelTitles?: boolean; columns?: string[]; - sort?: string[]; + sort?: string[][]; } export interface SearchOutput extends EmbeddableOutput { diff --git a/src/legacy/core_plugins/kibana/public/discover/types.d.ts b/src/legacy/core_plugins/kibana/public/discover/types.d.ts index 5922e2f8532fa9..6de969888f1660 100644 --- a/src/legacy/core_plugins/kibana/public/discover/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/discover/types.d.ts @@ -25,7 +25,7 @@ export interface SavedSearch { searchSource: SearchSource; description?: string; columns: string[]; - sort: string[]; + sort: string[][]; destroy: () => void; } export interface SavedSearchLoader { diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.js b/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.js index a792546dd7d03c..180a3a2d2dc943 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.js +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.js @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Markdown } from 'ui/markdown/markdown'; +import { Markdown } from '../../../../../kibana_react/public'; const whiteListedRules = ['backticks', 'emphasis', 'link', 'list']; diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.test.js b/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.test.js index fc3271fbf2cb6a..b0f495a6b492a3 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.test.js +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/content.test.js @@ -24,6 +24,14 @@ import { Content, } from './content'; + +jest.mock('../../../../../kibana_react/public', () => { + return { + Markdown: () =>
, + }; +}); + + test('should render content with markdown', () => { const component = shallow( { + return { + Markdown: () =>
, + }; +}); + test('render', () => { const component = shallowWithIntl( { + return { + Markdown: () =>
, + }; +}); + test('render', () => { const component = shallowWithIntl( { + return { + Markdown: () =>
, + }; +}); + function buildInstructionSet(type) { return { instructionSets: [ diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 55d1b0fadac8f7..bbe610e5e77559 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -65,6 +65,7 @@ export interface VisualizeOutput extends EmbeddableOutput { editUrl: string; indexPatterns?: StaticIndexPattern[]; savedObjectId: string; + visTypeName: string; } export class VisualizeEmbeddable extends Embeddable { @@ -99,6 +100,7 @@ export class VisualizeEmbeddable extends Embeddable { + return {}; +}); + describe('markdown vis controller', () => { it('should set html from markdown params', () => { const vis = { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx index ca04173d1cea6f..4e77bb196b713d 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { Markdown } from 'ui/markdown/markdown'; +import { Markdown } from '../../kibana_react/public'; import { MarkdownVisParams } from './types'; interface MarkdownVisComponentProps extends MarkdownVisParams { diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 671d0165abe9b9..34797cd90cc30c 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -18,9 +18,6 @@ */ import Joi from 'joi'; -import { - constants as cryptoConstants -} from 'crypto'; import os from 'os'; import { @@ -35,6 +32,7 @@ import { DEFAULT_CSP_WARN_LEGACY_BROWSERS, } from '../csp'; +const HANDLED_IN_NEW_PLATFORM = Joi.any().description('This key is handled in the new platform ONLY'); export default () => Joi.object({ pkg: Joi.object({ version: Joi.string().default(Joi.ref('$version')), @@ -83,43 +81,8 @@ export default () => Joi.object({ server: Joi.object({ uuid: Joi.string().guid().default(), name: Joi.string().default(os.hostname()), - host: Joi.string().hostname().default('localhost'), - port: Joi.number().default(5601), - keepaliveTimeout: Joi.number().default(120000), - socketTimeout: Joi.number().default(120000), - maxPayloadBytes: Joi.number().default(1048576), - autoListen: Joi.boolean().default(true), defaultRoute: Joi.string().default('/app/kibana').regex(/^\//, `start with a slash`), - basePath: Joi.string().default('').allow('').regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`), - rewriteBasePath: Joi.boolean().when('basePath', { - is: '', - then: Joi.default(false).valid(false), - otherwise: Joi.default(false), - }), customResponseHeaders: Joi.object().unknown(true).default({}), - ssl: Joi.object({ - enabled: Joi.boolean().default(false), - redirectHttpFromPort: Joi.number(), - certificate: Joi.string().when('enabled', { - is: true, - then: Joi.required(), - }), - key: Joi.string().when('enabled', { - is: true, - then: Joi.required() - }), - keyPassphrase: Joi.string(), - certificateAuthorities: Joi.array().single().items(Joi.string()).default([]), - supportedProtocols: Joi.array().items(Joi.string().valid('TLSv1', 'TLSv1.1', 'TLSv1.2')).default(['TLSv1.1', 'TLSv1.2']), - cipherSuites: Joi.array().items(Joi.string()).default(cryptoConstants.defaultCoreCipherList.split(':')) - }).default(), - cors: Joi.when('$dev', { - is: true, - then: Joi.object().default({ - origin: ['*://localhost:9876'] // karma test server - }), - otherwise: Joi.boolean().default(false) - }), xsrf: Joi.object({ disableProtection: Joi.boolean().default(false), whitelist: Joi.array().items( @@ -127,6 +90,25 @@ export default () => Joi.object({ ).default([]), token: Joi.string().optional().notes('Deprecated') }).default(), + + // keep them for BWC, remove when not used in Legacy. + // validation should be in sync with one in New platform. + // https://github.com/elastic/kibana/blob/master/src/core/server/http/http_config.ts + basePath: Joi.string().default('').allow('').regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`), + host: Joi.string().hostname().default('localhost'), + port: Joi.number().default(5601), + rewriteBasePath: Joi.boolean().when('basePath', { + is: '', + then: Joi.default(false).valid(false), + otherwise: Joi.default(false), + }), + + autoListen: HANDLED_IN_NEW_PLATFORM, + cors: HANDLED_IN_NEW_PLATFORM, + keepaliveTimeout: HANDLED_IN_NEW_PLATFORM, + maxPayloadBytes: HANDLED_IN_NEW_PLATFORM, + socketTimeout: HANDLED_IN_NEW_PLATFORM, + ssl: HANDLED_IN_NEW_PLATFORM, }).default(), uiSettings: Joi.object().keys({ diff --git a/src/legacy/server/config/schema.test.js b/src/legacy/server/config/schema.test.js index f27a25fefc585c..b30a99f66c1059 100644 --- a/src/legacy/server/config/schema.test.js +++ b/src/legacy/server/config/schema.test.js @@ -101,128 +101,6 @@ describe('Config schema', function () { }); }); - describe('ssl', function () { - describe('enabled', function () { - - it('can\'t be a string', function () { - const config = {}; - set(config, 'server.ssl.enabled', 'bogus'); - const { error } = validate(config); - expect(error).toBeInstanceOf(Object); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'enabled']); - }); - - it('can be true', function () { - const config = {}; - set(config, 'server.ssl.enabled', true); - set(config, 'server.ssl.certificate', '/path.cert'); - set(config, 'server.ssl.key', '/path.key'); - const { error } = validate(config); - expect(error).toBe(null); - }); - - it('can be false', function () { - const config = {}; - set(config, 'server.ssl.enabled', false); - const { error } = validate(config); - expect(error).toBe(null); - }); - }); - - describe('certificate', function () { - - it('isn\'t required when ssl isn\'t enabled', function () { - const config = {}; - set(config, 'server.ssl.enabled', false); - const { error } = validate(config); - expect(error).toBe(null); - }); - - it('is required when ssl is enabled', function () { - const config = {}; - set(config, 'server.ssl.enabled', true); - set(config, 'server.ssl.key', '/path.key'); - const { error } = validate(config); - expect(error).toBeInstanceOf(Object); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'certificate']); - }); - }); - - describe('key', function () { - it('isn\'t required when ssl isn\'t enabled', function () { - const config = {}; - set(config, 'server.ssl.enabled', false); - const { error } = validate(config); - expect(error).toBe(null); - }); - - it('is required when ssl is enabled', function () { - const config = {}; - set(config, 'server.ssl.enabled', true); - set(config, 'server.ssl.certificate', '/path.cert'); - const { error } = validate(config); - expect(error).toBeInstanceOf(Object); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'key']); - }); - }); - - describe('keyPassphrase', function () { - it('is a possible config value', function () { - const config = {}; - set(config, 'server.ssl.keyPassphrase', 'password'); - const { error } = validate(config); - expect(error).toBe(null); - }); - }); - - describe('certificateAuthorities', function () { - it('allows array of string', function () { - const config = {}; - set(config, 'server.ssl.certificateAuthorities', ['/path1.crt', '/path2.crt']); - const { error } = validate(config); - expect(error).toBe(null); - }); - - it('allows a single string', function () { - const config = {}; - set(config, 'server.ssl.certificateAuthorities', '/path1.crt'); - const { error } = validate(config); - expect(error).toBe(null); - }); - }); - - describe('supportedProtocols', function () { - - it ('rejects SSLv2', function () { - const config = {}; - set(config, 'server.ssl.supportedProtocols', ['SSLv2']); - const { error } = validate(config); - expect(error).toBeInstanceOf(Object); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'supportedProtocols', 0]); - }); - - it('rejects SSLv3', function () { - const config = {}; - set(config, 'server.ssl.supportedProtocols', ['SSLv3']); - const { error } = validate(config); - expect(error).toBeInstanceOf(Object); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'supportedProtocols', 0]); - }); - - it('accepts TLSv1, TLSv1.1, TLSv1.2', function () { - const config = {}; - set(config, 'server.ssl.supportedProtocols', ['TLSv1', 'TLSv1.1', 'TLSv1.2']); - const { error } = validate(config); - expect(error).toBe(null); - }); - }); - }); - describe('xsrf', () => { it('disableProtection is `false` by default.', () => { const { error, value: { server: { xsrf: { disableProtection } } } } = validate({}); diff --git a/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js b/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js index ac7a5e80c45a1e..87a1f1c9ece478 100644 --- a/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js @@ -212,10 +212,10 @@ export const getSavedObjects = () => [ "taxful_total_price", "total_quantity" ], - "sort": [ + "sort": [[ "order_date", "desc" - ], + ]], "version": 1, "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"index\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" diff --git a/src/legacy/server/sample_data/data_sets/flights/saved_objects.js b/src/legacy/server/sample_data/data_sets/flights/saved_objects.js index 726beeeb83227d..5157e19df593e1 100644 --- a/src/legacy/server/sample_data/data_sets/flights/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/flights/saved_objects.js @@ -84,10 +84,10 @@ export const getSavedObjects = () => [ "Cancelled", "FlightDelayType" ], - "sort": [ + "sort": [[ "timestamp", "desc" - ], + ]], "version": 1, "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"index\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 3ae2533eb6b9da..d898c06a73fdf4 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -19,7 +19,6 @@ @import './exit_full_screen/index'; @import './field_editor/index'; @import './kbn_top_nav/index'; -@import './markdown/index'; @import './notify/index'; @import './share/index'; @import './style_compile/index'; diff --git a/src/legacy/ui/public/index_patterns/index_patterns.ts b/src/legacy/ui/public/index_patterns/index_patterns.ts index 412a9b684a1055..daf81a66b9d405 100644 --- a/src/legacy/ui/public/index_patterns/index_patterns.ts +++ b/src/legacy/ui/public/index_patterns/index_patterns.ts @@ -18,15 +18,17 @@ */ import { idx } from '@kbn/elastic-idx'; -import { SavedObjectsClientContract, SimpleSavedObject } from 'src/core/public'; +import { + SavedObjectsClientContract, + SimpleSavedObject, + UiSettingsClientContract, +} from 'src/core/public'; // @ts-ignore import { fieldFormats } from '../registry/field_formats'; import { IndexPattern } from './_index_pattern'; import { createIndexPatternCache } from './_pattern_cache'; import { IndexPatternsApiClient } from './index_patterns_api_client'; -import { UiSettingsClientContract } from '../../../../core/public'; - const indexPatternCache = createIndexPatternCache(); const apiClient = new IndexPatternsApiClient(); diff --git a/src/legacy/ui/public/markdown/_github_markdown.scss b/src/legacy/ui/public/markdown/_github_markdown.scss deleted file mode 100644 index ec397b0834e7be..00000000000000 --- a/src/legacy/ui/public/markdown/_github_markdown.scss +++ /dev/null @@ -1,400 +0,0 @@ -// The MIT License (MIT) - -// Copyright (c) Sindre Sorhus (sindresorhus.com) - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - - -.kbnMarkdown__body { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - line-height: 1.5; - color: $euiTextColor; - word-wrap: break-word; - - &::before { - display: table; - content: ""; - } - - &::after { - display: table; - clear: both; - content: ""; - } - - > *:first-child { - margin-top: 0 !important; - } - - > *:last-child { - margin-bottom: 0 !important; - } - - a { - background-color: transparent; - -webkit-text-decoration-skip: objects; - color: $euiColorPrimary; - text-decoration: none; - - &:active, - &:hover { - color: darken($euiColorPrimary, 10%); - text-decoration: underline; - outline-width: 0; - } - - &:not([href]) { - color: inherit; - text-decoration: none; - } - } - - strong { - font-weight: $euiFontWeightBold; - } - - img { - border-style: none; - max-width: 100%; - box-sizing: content-box; - background-color: $euiColorEmptyShade; - } - - svg:not(:root) { - overflow: hidden; - } - - hr { - box-sizing: content-box; - overflow: hidden; - background: transparent; - height: $euiSizeXS; - padding: 0; - margin: $euiSizeL 0; - background-color: $euiColorLightShade; - border: 0; - - &::before { - display: table; - content: ""; - } - - &::after { - display: table; - clear: both; - content: ""; - } - } - - input { - font: inherit; - font-family: inherit; - font-size: inherit; - line-height: inherit; - margin: 0; - overflow: visible; - } - - [type="checkbox"] { - box-sizing: border-box; - padding: 0; - } - - table { - @include euiScrollBar; - border-spacing: 0; - border-collapse: collapse; - display: block; - width: 100%; - overflow: auto; - border-left: $euiBorderThin; - - th { - font-weight: $euiFontWeightMedium; - color: $euiColorMediumShade; - } - - td, - th { - padding: $euiSizeS $euiSizeM; - border-top: $euiBorderThin; - border-bottom: $euiBorderThin; - - &:last-child { - border-right: $euiBorderThin; - } - } - - tr { - background-color: transparent; - border-top: $euiBorderThin; - } - } - - h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: $euiFontWeightBold; - margin-top: $euiSizeL; - margin-bottom: $euiSize; - line-height: 1.25; - - &:hover .anchor { - text-decoration: none; - } - } - - h1 { - font-size: 2em; - padding-bottom: 0.3em; - border-bottom: $euiBorderThin; - } - - h2 { - font-size: 1.5em; - padding-bottom: 0.3em; - border-bottom: $euiBorderThin; - } - - h3 { - font-size: 1.25em; - } - - h4 { - font-size: 1em; - } - - h5 { - font-size: 0.875em; - } - - h6 { - font-size: 0.8em; - } - - p, - blockquote, - ul, - ol, - dl, - table, - pre { - margin-top: 0; - margin-bottom: $euiSize; - } - - blockquote { - font-size: 1em; - padding: 0 1em; - color: $euiColorDarkShade; - border-left: $euiBorderThick; - - > :first-child { - margin-top: 0; - } - - > :last-child { - margin-bottom: 0; - } - } - - ul, - ol { - padding-left: 2em; - margin-top: 0; - } - - ul { - list-style-type: disc; - } - - ol { - list-style-type: decimal; - } - - ol ol, - ul ol { - list-style-type: lower-roman; - } - - ul ul ol, - ul ol ol, - ol ul ol, - ol ol ol { - list-style-type: lower-alpha; - } - - ul ul, - ul ol, - ol ol, - ol ul { - margin-top: 0; - margin-bottom: 0; - } - - li > p { - margin-top: $euiSize; - } - - li + li { - margin-top: 0.25em; - } - - dd { - dt { - margin-top: $euiSize; - font-style: italic; - font-weight: $euiFontWeightBold; - } - - dd { - padding: 0 $euiSize; - margin-bottom: $euiSize; - } - } - - .anchor { - float: left; - padding-right: $euiSizeXS; - margin-left: -$euiSizeL; - line-height: 1; - - &:focus { - outline: none; - } - } - - code, - kbd, - pre { - font-family: $euiCodeFontFamily; - font-size: .85em; - background-color: $euiCodeBlockBackgroundColor; - color: $euiCodeBlockColor; - } - - kbd { - padding: 0.2em; - border: $euiBorderThin; - border-bottom-color: shadeOrTint($euiColorLightShade, 10%, 10%); - border-radius: $euiBorderRadius; - box-shadow: inset 0 -1px 0 $euiColorLightShade; - } - - code { - padding: 0.2em 0; - margin: 0; - border-radius: $euiBorderRadius; - - &::before, - &::after { - letter-spacing: -0.2em; - content: "\00a0"; - } - } - - pre { - @include euiScrollBar; - line-height: 1.45; - word-wrap: normal; - padding: $euiSize; - overflow: auto; - border-radius: $euiBorderRadius; - - code { - font-size: 1em; - word-break: normal; - white-space: pre; - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent !important; - border: 0; - - &::before, - &::after { - content: normal; - } - } - } - - .highlight { - margin-bottom: 1em; - - pre { - margin-bottom: 0; - word-break: normal; - } - } - - :checked + .radio-label { - position: relative; - z-index: 1; - border-color: $euiColorPrimary; - } - - .task-list-item { - list-style-type: none; - - + .task-list-item { - margin-top: $euiSizeXS; - } - - input { - margin: 0 0.2em 0.25em -1.6em; - vertical-align: middle; - } - } - - .pl-0 { - padding-left: 0 !important; - } - - .pl-1 { - padding-left: $euiSizeXS !important; - } - - .pl-2 { - padding-left: $euiSizeS !important; - } - - .pl-3 { - padding-left: $euiSize !important; - } - - .pl-4 { - padding-left: $euiSizeL !important; - } - - .pl-5 { - padding-left: $euiSizeXL !important; - } - - .pl-6 { - padding-left: $euiSizeXXL !important; - } -} diff --git a/src/legacy/ui/public/notify/notify.js b/src/legacy/ui/public/notify/notify.js index 5e6c12461dcc37..5afbf1111f6cea 100644 --- a/src/legacy/ui/public/notify/notify.js +++ b/src/legacy/ui/public/notify/notify.js @@ -18,7 +18,7 @@ */ import React from 'react'; -import { MarkdownSimple } from 'ui/markdown'; +import { MarkdownSimple } from '../../../core_plugins/kibana_react/public'; import chrome from '../chrome'; import { fatalError } from './fatal_error'; import { banners } from './banners'; diff --git a/src/legacy/ui/public/url/redirect_when_missing.js b/src/legacy/ui/public/url/redirect_when_missing.js index 3a203f079d70af..5fd8611dbcde94 100644 --- a/src/legacy/ui/public/url/redirect_when_missing.js +++ b/src/legacy/ui/public/url/redirect_when_missing.js @@ -20,7 +20,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { MarkdownSimple } from 'ui/markdown'; +import { MarkdownSimple } from '../../../core_plugins/kibana_react/public/markdown'; import { toastNotifications } from 'ui/notify'; import { SavedObjectNotFound } from '../errors'; import { uiModules } from '../modules'; diff --git a/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html b/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html index e57b0785954010..43c4793384bf13 100644 --- a/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html +++ b/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html @@ -16,8 +16,12 @@ - {{row.field}} - {{row.bucket}} + +
{{row.field}}
+ + +
{{row.bucket}} + {{row.metric}} diff --git a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html b/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html index d6054594ec776f..9e82739a57f0f2 100644 --- a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html +++ b/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html @@ -1,10 +1,11 @@ - + diff --git a/src/legacy/ui/public/vis/components/tooltip/_tooltip.scss b/src/legacy/ui/public/vis/components/tooltip/_tooltip.scss index 280c843247bd58..451ecc80844dde 100644 --- a/src/legacy/ui/public/vis/components/tooltip/_tooltip.scss +++ b/src/legacy/ui/public/vis/components/tooltip/_tooltip.scss @@ -22,7 +22,10 @@ table { td, th { + text-align: left; padding: $euiSizeXS; + overflow-wrap: break-word; + word-wrap: break-word; } } } @@ -41,6 +44,11 @@ margin-top: $euiSizeS; } } +.visTooltip__labelContainer, +.visTooltip__valueContainer { + overflow-wrap: break-word; + word-wrap: break-word; +} .visTooltip__headerIcon { flex: 0 0 auto; diff --git a/src/legacy/ui/public/vis/components/tooltip/tooltip.js b/src/legacy/ui/public/vis/components/tooltip/tooltip.js index 6008d5cf7961ab..7c395a2f4dc0e4 100644 --- a/src/legacy/ui/public/vis/components/tooltip/tooltip.js +++ b/src/legacy/ui/public/vis/components/tooltip/tooltip.js @@ -22,9 +22,14 @@ import _ from 'lodash'; import { Binder } from '../../../binder'; import { positionTooltip } from './position_tooltip'; import $ from 'jquery'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; let allContents = []; +const tooltipColumnPadding = parseInt(theme.euiSizeXS || 0, 10) * 2; +const tooltipTableMargin = parseInt(theme.euiSizeS || 0, 10) * 2; +const tooltipMaxWidth = parseInt(theme.euiSizeXL || 0, 10) * 10; + /** * Add tooltip and listeners to visualization elements * @@ -97,6 +102,46 @@ Tooltip.prototype.show = function () { left: placement.left, top: placement.top }); + // The number of columns on the tooltip is currently the only + // thing that differenciate one tooltip; from another + const tooltipColumns = $tooltip.find('tbody > tr:nth-of-type(1) > td').length; + if (tooltipColumns === 2) { + // on pointseries tooltip + const tooltipWidth = $tooltip.outerWidth(); + // get the last column to the right + const valueColumn = $tooltip.find('tr:nth-of-type(1) > td:nth-child(2)'); + if (valueColumn.length !== 1) { + return; + } + const valueColumnSize = valueColumn.outerWidth(); + const isGratherThanHalf = valueColumnSize > tooltipWidth / 2; + const containerMaxWidth = isGratherThanHalf + ? tooltipWidth / 2 - tooltipTableMargin - tooltipColumnPadding * 2 + : tooltipWidth - valueColumnSize - tooltipTableMargin - tooltipColumnPadding; + + $tooltip.find('.visTooltip__labelContainer').css({ + 'max-width': containerMaxWidth, + }); + if (isGratherThanHalf && tooltipWidth === tooltipMaxWidth) { + $tooltip.find('.visTooltip__valueContainer').css({ + 'max-width': containerMaxWidth, + }); + } + } else if(tooltipColumns === 3) { + // on hierarchical tooltip + const tooltipWidth = $tooltip.outerWidth(); + // get the last column to the right (3rd column) + const valueColumn = $tooltip.find('tr:nth-of-type(1) > td:nth-child(3)'); + if (valueColumn.length !== 1) { + return; + } + const valueColumnSize = valueColumn.outerWidth(); + const containerMaxWidth = (tooltipWidth - valueColumnSize - tooltipTableMargin) / 2 - tooltipColumnPadding; + + $tooltip.find('.visTooltip__labelContainer').css({ + 'max-width': containerMaxWidth + }); + } }; /** diff --git a/src/legacy/ui/public/vis/editors/default/default.js b/src/legacy/ui/public/vis/editors/default/default.js index da8ab50b7805ce..af0ebc65680c39 100644 --- a/src/legacy/ui/public/vis/editors/default/default.js +++ b/src/legacy/ui/public/vis/editors/default/default.js @@ -126,7 +126,9 @@ const defaultEditor = function ($rootScope, $compile) { return $scope.vis.getSerializableState($scope.state); }, function (newState) { $scope.vis.dirty = !angular.equals(newState, $scope.oldState); - $scope.metricAggs = $scope.state.aggs.getResponseAggs().filter(agg => + const responseAggs = $scope.state.aggs.getResponseAggs(); + $scope.hasHistogramAgg = responseAggs.some(agg => agg.type.name === 'histogram'); + $scope.metricAggs = responseAggs.filter(agg => _.get(agg, 'schema.group') === AggGroupNames.Metrics); const lastParentPipelineAgg = _.findLast($scope.metricAggs, ({ type }) => type.subtype === parentPipelineAggHelper.subtype); $scope.lastParentPipelineAggTitle = lastParentPipelineAgg && lastParentPipelineAgg.type.title; diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.html b/src/legacy/ui/public/vis/editors/default/sidebar.html index 14e9bd46c376d8..07c2e2b1730ee3 100644 --- a/src/legacy/ui/public/vis/editors/default/sidebar.html +++ b/src/legacy/ui/public/vis/editors/default/sidebar.html @@ -175,6 +175,7 @@ @@ -55,6 +57,7 @@ uiModules $scope.editor : ` diff --git a/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx index e82a0fdc0892d4..a2212f924cf47d 100644 --- a/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx +++ b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx @@ -20,6 +20,7 @@ import { Vis } from './../..'; export interface VisOptionsProps { + hasHistogramAgg: boolean; stateParams: VisParamType; vis: Vis; setValue(paramName: T, value: VisParamType[T]): void; diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js index 7e14721a354cb3..54442992150821 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.js @@ -349,8 +349,7 @@ export default function ({ getService }) { }); }); - // FAILING: https://github.com/elastic/kibana/issues/43163 - describe.skip('10,001 objects', () => { + describe('10,001 objects', () => { let customVisId; before(async () => { await esArchiver.load('saved_objects/10k'); diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 94eac40fa25416..3cb5d0a71cce56 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -148,6 +148,8 @@ export default function ({ getService, getPageObjects }) { const time = await PageObjects.timePicker.getTimeConfig(); expect(time.start).to.be('Sep 20, 2015 @ 00:00:00.000'); expect(time.end).to.be('Sep 20, 2015 @ 03:00:00.000'); + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.have.string('Sep 20, 2015 @ 02:57:03.761'); }); it('should modify the time range when the histogram is brushed', async function () { @@ -157,9 +159,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.waitForVisualization(); const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - if (newDurationHours < 1 || newDurationHours >= 5) { - throw new Error(`expected new duration of ${newDurationHours} hours to be between 1 and 5 hours`); - } + expect(Math.round(newDurationHours)).to.be(3); + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData).to.have.string('Sep 20, 2015 @ 02:56:02.323'); }); it('should show correct initial chart interval of Auto', async function () { diff --git a/test/functional/page_objects/point_series_page.js b/test/functional/page_objects/point_series_page.js index 7de7ebdd1e0117..371913f87fde7d 100644 --- a/test/functional/page_objects/point_series_page.js +++ b/test/functional/page_objects/point_series_page.js @@ -74,10 +74,11 @@ export function PointSeriesPageProvider({ getService }) { } async toggleGridCategoryLines() { - return await find.clickByCssSelector('#showCategoryLines'); + return await testSubjects.click('showCategoryLines'); } async setGridValueAxis(axis) { + log.debug(`setGridValueAxis(${axis})`); return await find.clickByCssSelector(`select#gridAxis option[value="${axis}"]`); } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index f8c37c792d51cc..24f8ce79d70a62 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,6 +2,7 @@ "prefix": "xpack", "paths": { "xpack.actions": "legacy/plugins/actions", + "xpack.advancedUiActions": "legacy/plugins/advanced_ui_actions", "xpack.alerting": "legacy/plugins/alerting", "xpack.apm": "legacy/plugins/apm", "xpack.beatsManagement": "legacy/plugins/beats_management", diff --git a/x-pack/index.js b/x-pack/index.js index 9b62fc18616f78..e1c12e75e053c4 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -43,6 +43,7 @@ import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects' import { snapshotRestore } from './legacy/plugins/snapshot_restore'; import { actions } from './legacy/plugins/actions'; import { alerting } from './legacy/plugins/alerting'; +import { advancedUiActions } from './legacy/plugins/advanced_ui_actions'; module.exports = function (kibana) { return [ @@ -85,5 +86,6 @@ module.exports = function (kibana) { snapshotRestore(kibana), actions(kibana), alerting(kibana), + advancedUiActions(kibana), ]; }; diff --git a/x-pack/legacy/plugins/actions/README.md b/x-pack/legacy/plugins/actions/README.md index f17456d6ea3939..8b1be7f6cbb35d 100644 --- a/x-pack/legacy/plugins/actions/README.md +++ b/x-pack/legacy/plugins/actions/README.md @@ -21,7 +21,7 @@ action types. 1. Develop and register an action type (see action types -> example). 2. Create an action by using the RESTful API (see actions -> create action). -3. Use alerts to fire actions or fire manually (see firing actions). +3. Use alerts to execute actions or execute manually (see firing actions). ## Action types @@ -51,7 +51,7 @@ This is the primary function for an action type. Whenever the action needs to ex |Property|Description| |---|---| |config|The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type.| -|params|Parameters for the execution. These will be given at fire time by either an alert or manually provided when calling the plugin provided fire function.| +|params|Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function.| |services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.

**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

**NOTE**: This currently only works when security is disabled. A future PR will add support for enabling security using Elasticsearch API tokens.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| @@ -119,13 +119,13 @@ Payload: |config|The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined.|object| |secrets|The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined.|object| -#### `POST /api/action/{id}/_fire`: Fire action +#### `POST /api/action/{id}/_execute`: Execute action Params: |Property|Description|Type| |---|---|---| -|id|The id of the action you're trying to fire.|string| +|id|The id of the action you're trying to execute.|string| Payload: @@ -135,24 +135,24 @@ Payload: ## Firing actions -The plugin exposes a fire function that you can use to fire actions. +The plugin exposes an execute function that you can use to run actions. -**server.plugins.actions.fire(options)** +**server.plugins.actions.execute(options)** The following table describes the properties of the `options` object. |Property|Description|Type| |---|---|---| -|id|The id of the action you want to fire.|string| +|id|The id of the action you want to execute.|string| |params|The `params` value to give the action type executor.|object| |spaceId|The space id the action is within.|string| ### Example -This example makes action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` fire an email. The action plugin will load the saved object and find what action type to call with `params`. +This example makes action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` send an email. The action plugin will load the saved object and find what action type to call with `params`. ``` -server.plugins.actions.fire({ +server.plugins.actions.execute({ id: '3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5', spaceId: 'default', // The spaceId of the action params: { diff --git a/x-pack/legacy/plugins/actions/server/create_fire_function.test.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts similarity index 92% rename from x-pack/legacy/plugins/actions/server/create_fire_function.test.ts rename to x-pack/legacy/plugins/actions/server/create_execute_function.test.ts index 1f286d371e6333..176eb7d1c855ab 100644 --- a/x-pack/legacy/plugins/actions/server/create_fire_function.test.ts +++ b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts @@ -5,7 +5,7 @@ */ import { taskManagerMock } from '../../task_manager/task_manager.mock'; -import { createFireFunction } from './create_fire_function'; +import { createExecuteFunction } from './create_execute_function'; import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; const mockTaskManager = taskManagerMock.create(); @@ -14,9 +14,9 @@ const spaceIdToNamespace = jest.fn(); beforeEach(() => jest.resetAllMocks()); -describe('fire()', () => { +describe('execute()', () => { test('schedules the action with all given parameters', async () => { - const fireFn = createFireFunction({ + const executeFn = createExecuteFunction({ taskManager: mockTaskManager, internalSavedObjectsRepository: savedObjectsClient, spaceIdToNamespace, @@ -30,7 +30,7 @@ describe('fire()', () => { references: [], }); spaceIdToNamespace.mockReturnValueOnce('namespace1'); - await fireFn({ + await executeFn({ id: '123', params: { baz: false }, spaceId: 'default', diff --git a/x-pack/legacy/plugins/actions/server/create_fire_function.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.ts similarity index 82% rename from x-pack/legacy/plugins/actions/server/create_fire_function.ts rename to x-pack/legacy/plugins/actions/server/create_execute_function.ts index 0bf11cbf5dd408..5f4f568c7e3fb4 100644 --- a/x-pack/legacy/plugins/actions/server/create_fire_function.ts +++ b/x-pack/legacy/plugins/actions/server/create_execute_function.ts @@ -8,24 +8,24 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { TaskManager } from '../../task_manager'; import { SpacesPlugin } from '../../spaces'; -interface CreateFireFunctionOptions { +interface CreateExecuteFunctionOptions { taskManager: TaskManager; internalSavedObjectsRepository: SavedObjectsClientContract; spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; } -export interface FireOptions { +export interface ExecuteOptions { id: string; params: Record; spaceId: string; } -export function createFireFunction({ +export function createExecuteFunction({ taskManager, internalSavedObjectsRepository, spaceIdToNamespace, -}: CreateFireFunctionOptions) { - return async function fire({ id, params, spaceId }: FireOptions) { +}: CreateExecuteFunctionOptions) { + return async function execute({ id, params, spaceId }: ExecuteOptions) { const namespace = spaceIdToNamespace(spaceId); const actionSavedObject = await internalSavedObjectsRepository.get('action', id, { namespace }); await taskManager.schedule({ diff --git a/x-pack/legacy/plugins/actions/server/init.ts b/x-pack/legacy/plugins/actions/server/init.ts index 3316b69923e774..9004593f498659 100644 --- a/x-pack/legacy/plugins/actions/server/init.ts +++ b/x-pack/legacy/plugins/actions/server/init.ts @@ -7,7 +7,7 @@ import { Legacy } from 'kibana'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; -import { createFireFunction } from './create_fire_function'; +import { createExecuteFunction } from './create_execute_function'; import { ActionsPlugin, Services } from './types'; import { createRoute, @@ -16,7 +16,7 @@ import { getRoute, updateRoute, listActionTypesRoute, - fireRoute, + executeRoute, } from './routes'; import { registerBuiltInActionTypes } from './builtin_action_types'; import { SpacesPlugin } from '../../spaces'; @@ -86,13 +86,13 @@ export function init(server: Legacy.Server) { findRoute(server); updateRoute(server); listActionTypesRoute(server); - fireRoute({ + executeRoute({ server, actionTypeRegistry, getServices, }); - const fireFn = createFireFunction({ + const executeFn = createExecuteFunction({ taskManager: taskManager!, internalSavedObjectsRepository: savedObjectsRepositoryWithInternalUser, spaceIdToNamespace(...args) { @@ -111,7 +111,7 @@ export function init(server: Legacy.Server) { return actionsClient; }); const exposedFunctions: ActionsPlugin = { - fire: fireFn, + execute: executeFn, registerType: actionTypeRegistry.register.bind(actionTypeRegistry), listTypes: actionTypeRegistry.list.bind(actionTypeRegistry), }; diff --git a/x-pack/legacy/plugins/actions/server/routes/fire.test.ts b/x-pack/legacy/plugins/actions/server/routes/execute.test.ts similarity index 89% rename from x-pack/legacy/plugins/actions/server/routes/fire.test.ts rename to x-pack/legacy/plugins/actions/server/routes/execute.test.ts index bd994134c1d91c..7ca856b0a05baa 100644 --- a/x-pack/legacy/plugins/actions/server/routes/fire.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/execute.test.ts @@ -9,22 +9,22 @@ jest.mock('../lib/execute', () => ({ })); import { createMockServer } from './_mock_server'; -import { fireRoute } from './fire'; +import { executeRoute } from './execute'; const getServices = jest.fn(); const { server, actionTypeRegistry, savedObjectsClient } = createMockServer(); -fireRoute({ server, actionTypeRegistry, getServices }); +executeRoute({ server, actionTypeRegistry, getServices }); beforeEach(() => jest.resetAllMocks()); -it('fires an action with proper parameters', async () => { +it('executes an action with proper parameters', async () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { execute } = require('../lib/execute'); const request = { method: 'POST', - url: '/api/action/1/_fire', + url: '/api/action/1/_execute', payload: { params: { foo: true, diff --git a/x-pack/legacy/plugins/actions/server/routes/fire.ts b/x-pack/legacy/plugins/actions/server/routes/execute.ts similarity index 86% rename from x-pack/legacy/plugins/actions/server/routes/fire.ts rename to x-pack/legacy/plugins/actions/server/routes/execute.ts index 50d4aabe91126b..ac06300d68d731 100644 --- a/x-pack/legacy/plugins/actions/server/routes/fire.ts +++ b/x-pack/legacy/plugins/actions/server/routes/execute.ts @@ -9,7 +9,7 @@ import Hapi from 'hapi'; import { execute } from '../lib'; import { ActionTypeRegistryContract, GetServicesFunction } from '../types'; -interface FireRequest extends Hapi.Request { +interface ExecuteRequest extends Hapi.Request { params: { id: string; }; @@ -18,16 +18,16 @@ interface FireRequest extends Hapi.Request { }; } -interface FireRouteOptions { +interface ExecuteRouteOptions { server: Hapi.Server; actionTypeRegistry: ActionTypeRegistryContract; getServices: GetServicesFunction; } -export function fireRoute({ server, actionTypeRegistry, getServices }: FireRouteOptions) { +export function executeRoute({ server, actionTypeRegistry, getServices }: ExecuteRouteOptions) { server.route({ method: 'POST', - path: '/api/action/{id}/_fire', + path: '/api/action/{id}/_execute', options: { response: { emptyStatusCode: 204, @@ -48,7 +48,7 @@ export function fireRoute({ server, actionTypeRegistry, getServices }: FireRoute .required(), }, }, - async handler(request: FireRequest, h: Hapi.ResponseToolkit) { + async handler(request: ExecuteRequest, h: Hapi.ResponseToolkit) { const { id } = request.params; const { params } = request.payload; const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); diff --git a/x-pack/legacy/plugins/actions/server/routes/index.ts b/x-pack/legacy/plugins/actions/server/routes/index.ts index 793c11a2874138..3056fe7c78d5a8 100644 --- a/x-pack/legacy/plugins/actions/server/routes/index.ts +++ b/x-pack/legacy/plugins/actions/server/routes/index.ts @@ -10,4 +10,4 @@ export { findRoute } from './find'; export { getRoute } from './get'; export { updateRoute } from './update'; export { listActionTypesRoute } from './list_action_types'; -export { fireRoute } from './fire'; +export { executeRoute } from './execute'; diff --git a/x-pack/legacy/plugins/actions/server/types.ts b/x-pack/legacy/plugins/actions/server/types.ts index a3134b4cd4d1f5..e30287d6dde603 100644 --- a/x-pack/legacy/plugins/actions/server/types.ts +++ b/x-pack/legacy/plugins/actions/server/types.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; -import { FireOptions } from './create_fire_function'; +import { ExecuteOptions } from './create_execute_function'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (basePath: string, overwrites?: Partial) => Services; @@ -21,7 +21,7 @@ export interface Services { export interface ActionsPlugin { registerType: ActionTypeRegistry['register']; listTypes: ActionTypeRegistry['list']; - fire(options: FireOptions): Promise; + execute(options: ExecuteOptions): Promise; } // the parameters passed to an action type executor function diff --git a/x-pack/legacy/plugins/advanced_ui_actions/index.ts b/x-pack/legacy/plugins/advanced_ui_actions/index.ts new file mode 100644 index 00000000000000..e7b54d8863456e --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; + +export const advancedUiActions = (kibana: any) => + new kibana.Plugin({ + id: 'advanced_ui_actions', + publicDir: resolve(__dirname, 'public'), + uiExports: { + hacks: 'plugins/advanced_ui_actions/np_ready/public/legacy', + }, + }); diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/kibana.json b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/kibana.json new file mode 100644 index 00000000000000..f0303e7ad6a7ac --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "advanced_ui_actions", + "version": "kibana", + "requiredPlugins": [ + "embeddable" + ], + "server": false, + "ui": true +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.test.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.test.ts new file mode 100644 index 00000000000000..fe6a37c92f84be --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { canInheritTimeRange } from './can_inherit_time_range'; +import { + HelloWorldEmbeddable, + HelloWorldContainer, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples'; +import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers'; + +jest.mock('ui/new_platform'); + +test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => { + const embeddable = new TimeRangeEmbeddable( + { id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } }, + new HelloWorldContainer({ id: '123', panels: {} }, (() => null) as any) + ); + + expect(canInheritTimeRange(embeddable)).toBe(false); +}); + +test('canInheritTimeRange returns false if embeddable is without a time range', () => { + const embeddable = new HelloWorldEmbeddable( + { id: '1234' }, + new HelloWorldContainer({ id: '123', panels: {} }, (() => null) as any) + ); + // @ts-ignore + expect(canInheritTimeRange(embeddable)).toBe(false); +}); + +test('canInheritTimeRange returns true if embeddable is inside a container with a time range', () => { + const embeddable = new TimeRangeEmbeddable( + { id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } }, + new TimeRangeContainer( + { id: '123', panels: {}, timeRange: { from: 'noxw-15m', to: 'now' } }, + (() => null) as any + ) + ); + expect(canInheritTimeRange(embeddable)).toBe(true); +}); diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.ts new file mode 100644 index 00000000000000..221fdc314f8c84 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimeRange } from '../../../../../../../src/plugins/data/public'; +import { + Embeddable, + IContainer, + ContainerInput, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TimeRangeInput } from './custom_time_range_action'; + +interface ContainerTimeRangeInput extends ContainerInput { + timeRange: TimeRange; +} + +export function canInheritTimeRange(embeddable: Embeddable) { + if (!embeddable.parent) { + return false; + } + + const parent = embeddable.parent as IContainer; + + return parent.getInput().timeRange !== undefined; +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.test.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.test.ts new file mode 100644 index 00000000000000..f553ac1c0b2d7e --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.test.ts @@ -0,0 +1,345 @@ +/* + * 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-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { skip } from 'rxjs/operators'; +import * as Rx from 'rxjs'; +import { mount } from 'enzyme'; + +import { EmbeddableFactory } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers'; +import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory'; +import { CustomTimeRangeAction } from './custom_time_range_action'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { + HelloWorldEmbeddableFactory, + HELLO_WORLD_EMBEDDABLE_TYPE, + HelloWorldEmbeddable, + HelloWorldContainer, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples'; + +import { nextTick } from 'test_utils/enzyme_helpers'; +import { ReactElement } from 'react'; + +jest.mock('ui/new_platform'); + +test('Custom time range action prevents embeddable from using container time', async done => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + }, + }, + '2': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '2', + }, + }, + }, + id: '123', + }, + (() => {}) as any + ); + + await container.untilEmbeddableLoaded('1'); + await container.untilEmbeddableLoaded('2'); + + const child1 = container.getChild('1'); + expect(child1).toBeDefined(); + expect(child1.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' }); + + const child2 = container.getChild('2'); + expect(child2).toBeDefined(); + expect(child2.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' }); + + const start = coreMock.createStart(); + const overlayMock = start.overlays; + overlayMock.openModal.mockClear(); + new CustomTimeRangeAction({ + openModal: start.overlays.openModal, + commonlyUsedRanges: [], + dateFormat: 'MM YYY', + }).execute({ + embeddable: child1, + }); + + await nextTick(); + const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement; + + const wrapper = mount(openModal); + wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } }); + + findTestSubject(wrapper, 'addPerPanelTimeRangeButton').simulate('click'); + + const subscription = Rx.merge(container.getOutput$(), container.getInput$()) + .pipe(skip(2)) + .subscribe(() => { + expect(child1.getInput().timeRange).toEqual({ from: 'now-30days', to: 'now-29days' }); + expect(child2.getInput().timeRange).toEqual({ from: 'now-30m', to: 'now-1m' }); + subscription.unsubscribe(); + done(); + }); + + container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } }); +}); + +test('Removing custom time range action resets embeddable back to container time', async done => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + }, + }, + '2': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '2', + }, + }, + }, + id: '123', + }, + (() => {}) as any + ); + + await container.untilEmbeddableLoaded('1'); + await container.untilEmbeddableLoaded('2'); + + const child1 = container.getChild('1'); + const child2 = container.getChild('2'); + + const start = coreMock.createStart(); + const overlayMock = start.overlays; + overlayMock.openModal.mockClear(); + new CustomTimeRangeAction({ + openModal: start.overlays.openModal, + commonlyUsedRanges: [], + dateFormat: 'MM YYY', + }).execute({ + embeddable: child1, + }); + + await nextTick(); + const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement; + + const wrapper = mount(openModal); + wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } }); + + findTestSubject(wrapper, 'addPerPanelTimeRangeButton').simulate('click'); + + container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } }); + + new CustomTimeRangeAction({ + openModal: start.overlays.openModal, + commonlyUsedRanges: [], + dateFormat: 'MM YYY', + }).execute({ + embeddable: child1, + }); + + await nextTick(); + const openModal2 = (overlayMock.openModal as any).mock.calls[1][0]; + + const wrapper2 = mount(openModal2); + findTestSubject(wrapper2, 'removePerPanelTimeRangeButton').simulate('click'); + + const subscription = Rx.merge(container.getOutput$(), container.getInput$()) + .pipe(skip(2)) + .subscribe(() => { + expect(child1.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' }); + expect(child2.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' }); + subscription.unsubscribe(); + done(); + }); + + container.updateInput({ timeRange: { from: 'now-10m', to: 'now-5m' } }); +}); + +test('Cancelling custom time range action leaves state alone', async done => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + timeRange: { to: '2', from: '1' }, + }, + }, + '2': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '2', + }, + }, + }, + id: '123', + }, + (() => {}) as any + ); + + await container.untilEmbeddableLoaded('1'); + await container.untilEmbeddableLoaded('2'); + + const child1 = container.getChild('1'); + const child2 = container.getChild('2'); + + const start = coreMock.createStart(); + const overlayMock = start.overlays; + overlayMock.openModal.mockClear(); + new CustomTimeRangeAction({ + openModal: start.overlays.openModal, + commonlyUsedRanges: [], + dateFormat: 'MM YYY', + }).execute({ + embeddable: child1, + }); + + await nextTick(); + const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement; + + const wrapper = mount(openModal); + wrapper.setState({ timeRange: { from: 'now-300m', to: 'now-400m' } }); + + findTestSubject(wrapper, 'cancelPerPanelTimeRangeButton').simulate('click'); + + const subscription = Rx.merge(container.getOutput$(), container.getInput$()) + .pipe(skip(2)) + .subscribe(() => { + expect(child1.getInput().timeRange).toEqual({ from: '1', to: '2' }); + expect(child2.getInput().timeRange).toEqual({ from: 'now-30m', to: 'now-1m' }); + subscription.unsubscribe(); + done(); + }); + + container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } }); +}); + +test(`badge is compatible with embeddable that inherits from parent`, async () => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + }, + }, + }, + id: '123', + }, + (() => {}) as any + ); + + await container.untilEmbeddableLoaded('1'); + + const child = container.getChild('1'); + + const start = coreMock.createStart(); + const compatible = await new CustomTimeRangeAction({ + openModal: start.overlays.openModal, + commonlyUsedRanges: [], + dateFormat: 'MM YYY', + }).isCompatible({ + embeddable: child, + }); + expect(compatible).toBe(true); +}); + +// TODO: uncomment when https://github.com/elastic/kibana/issues/43271 is fixed. +// test('Embeddable that does not use time range in a container that has time range is incompatible', async () => { +// const embeddableFactories = new Map(); +// embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory()); +// const container = new TimeRangeContainer( +// { +// timeRange: { from: 'now-15m', to: 'now' }, +// panels: { +// '1': { +// type: HELLO_WORLD_EMBEDDABLE_TYPE, +// explicitInput: { +// id: '1', +// }, +// }, +// }, +// id: '123', +// }, +// (() => null) as any +// ); + +// await container.untilEmbeddableLoaded('1'); + +// const child = container.getChild('1'); + +// const start = coreMock.createStart(); +// const action = await new CustomTimeRangeAction({ +// openModal: start.overlays.openModal, +// dateFormat: 'MM YYYY', +// commonlyUsedRanges: [], +// }); + +// async function check() { +// await action.execute({ embeddable: child }); +// } +// await expect(check()).rejects.toThrow(Error); +// }); + +test('Attempting to execute on incompatible embeddable throws an error', async () => { + const embeddableFactories = new Map(); + embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory()); + const container = new HelloWorldContainer( + { + panels: { + '1': { + type: HELLO_WORLD_EMBEDDABLE_TYPE, + explicitInput: { + id: '1', + }, + }, + }, + id: '123', + }, + (() => null) as any + ); + + await container.untilEmbeddableLoaded('1'); + + const child = container.getChild('1'); + + const start = coreMock.createStart(); + const action = await new CustomTimeRangeAction({ + openModal: start.overlays.openModal, + dateFormat: 'MM YYYY', + commonlyUsedRanges: [], + }); + + async function check() { + await action.execute({ embeddable: child }); + } + await expect(check()).rejects.toThrow(Error); +}); diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.tsx b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.tsx new file mode 100644 index 00000000000000..ae3cbbc0591190 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.tsx @@ -0,0 +1,112 @@ +/* + * 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 React from 'react'; +import { TimeRange } from '../../../../../../../src/plugins/data/public'; +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; +import { VisualizeEmbeddable } from '../../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable'; +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable/constants'; + +import { + Action, + IEmbeddable, + ActionContext, + IncompatibleActionError, + Embeddable, + EmbeddableInput, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; + +import { CustomizeTimeRangeModal } from './customize_time_range_modal'; +import { OpenModal, CommonlyUsedRange } from './types'; + +const CUSTOM_TIME_RANGE = 'CUSTOM_TIME_RANGE'; + +export interface TimeRangeInput extends EmbeddableInput { + timeRange: TimeRange; +} + +function hasTimeRange( + embeddable: IEmbeddable | Embeddable +): embeddable is Embeddable { + return (embeddable as Embeddable).getInput().timeRange !== undefined; +} + +function isVisualizeEmbeddable( + embeddable: IEmbeddable | VisualizeEmbeddable +): embeddable is VisualizeEmbeddable { + return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE; +} + +export class CustomTimeRangeAction extends Action { + public readonly type = CUSTOM_TIME_RANGE; + private openModal: OpenModal; + private dateFormat?: string; + private commonlyUsedRanges: CommonlyUsedRange[]; + + constructor({ + openModal, + dateFormat, + commonlyUsedRanges, + }: { + openModal: OpenModal; + dateFormat: string; + commonlyUsedRanges: CommonlyUsedRange[]; + }) { + super(CUSTOM_TIME_RANGE); + this.order = 7; + this.openModal = openModal; + this.dateFormat = dateFormat; + this.commonlyUsedRanges = commonlyUsedRanges; + } + + public getDisplayName() { + return i18n.translate('xpack.advancedUiActions.customizeTimeRangeMenuItem.displayName', { + defaultMessage: 'Customize time range', + }); + } + + public getIconType() { + return 'calendar'; + } + + public async isCompatible({ embeddable }: ActionContext) { + const isInputControl = + isVisualizeEmbeddable(embeddable) && + embeddable.getOutput().visTypeName === 'input_control_vis'; + + const isMarkdown = + isVisualizeEmbeddable(embeddable) && embeddable.getOutput().visTypeName === 'markdown'; + return Boolean( + embeddable && + hasTimeRange(embeddable) && + // Saved searches don't listen to the time range from the container that is passed down to them so it + // won't work without a fix. For now, just leave them out. + embeddable.type !== SEARCH_EMBEDDABLE_TYPE && + !isInputControl && + !isMarkdown + ); + } + + public async execute({ embeddable }: ActionContext) { + const isCompatible = await this.isCompatible({ embeddable }); + if (!isCompatible) { + throw new IncompatibleActionError(); + } + + // Only here for typescript + if (hasTimeRange(embeddable)) { + const modalSession = this.openModal( + modalSession.close()} + embeddable={embeddable} + dateFormat={this.dateFormat} + commonlyUsedRanges={this.commonlyUsedRanges} + /> + ); + } + } +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.test.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.test.ts new file mode 100644 index 00000000000000..33a6adb61775a3 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.test.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. + */ + +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { skip } from 'rxjs/operators'; +import * as Rx from 'rxjs'; +import { mount } from 'enzyme'; + +import { EmbeddableFactory } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers'; +import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory'; +import { CustomTimeRangeBadge } from './custom_time_range_badge'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { ReactElement } from 'react'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +test('Removing custom time range from badge resets embeddable back to container time', async done => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + timeRange: { from: '1', to: '2' }, + }, + }, + '2': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '2', + }, + }, + }, + id: '123', + }, + (() => null) as any + ); + + await container.untilEmbeddableLoaded('1'); + await container.untilEmbeddableLoaded('2'); + + const child1 = container.getChild('1'); + const child2 = container.getChild('2'); + + const start = coreMock.createStart(); + const overlayMock = start.overlays; + overlayMock.openModal.mockClear(); + new CustomTimeRangeBadge({ + openModal: start.overlays.openModal, + dateFormat: 'MM YYYY', + commonlyUsedRanges: [], + }).execute({ + embeddable: child1, + }); + + await nextTick(); + const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement; + + const wrapper = mount(openModal); + findTestSubject(wrapper, 'removePerPanelTimeRangeButton').simulate('click'); + + const subscription = Rx.merge(child1.getInput$(), container.getOutput$(), container.getInput$()) + .pipe(skip(4)) + .subscribe(() => { + expect(child1.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' }); + expect(child2.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' }); + subscription.unsubscribe(); + done(); + }); + + container.updateInput({ timeRange: { from: 'now-10m', to: 'now-5m' } }); +}); + +test(`badge is not compatible with embeddable that inherits from parent`, async () => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + }, + }, + }, + id: '123', + }, + (() => null) as any + ); + + await container.untilEmbeddableLoaded('1'); + + const child = container.getChild('1'); + + const start = coreMock.createStart(); + const compatible = await new CustomTimeRangeBadge({ + openModal: start.overlays.openModal, + dateFormat: 'MM YYYY', + commonlyUsedRanges: [], + }).isCompatible({ + embeddable: child, + }); + expect(compatible).toBe(false); +}); + +test(`badge is compatible with embeddable that has custom time range`, async () => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + timeRange: { to: '123', from: '456' }, + }, + }, + }, + id: '123', + }, + (() => null) as any + ); + + await container.untilEmbeddableLoaded('1'); + + const child = container.getChild('1'); + + const start = coreMock.createStart(); + const compatible = await new CustomTimeRangeBadge({ + openModal: start.overlays.openModal, + dateFormat: 'MM YYYY', + commonlyUsedRanges: [], + }).isCompatible({ + embeddable: child, + }); + expect(compatible).toBe(true); +}); + +test('Attempting to execute on incompatible embeddable throws an error', async () => { + const embeddableFactories = new Map(); + embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); + const container = new TimeRangeContainer( + { + timeRange: { from: 'now-15m', to: 'now' }, + panels: { + '1': { + type: TIME_RANGE_EMBEDDABLE, + explicitInput: { + id: '1', + }, + }, + }, + id: '123', + }, + (() => null) as any + ); + + await container.untilEmbeddableLoaded('1'); + + const child = container.getChild('1'); + + const start = coreMock.createStart(); + const badge = await new CustomTimeRangeBadge({ + openModal: start.overlays.openModal, + dateFormat: 'MM YYYY', + commonlyUsedRanges: [], + }); + + async function check() { + await badge.execute({ embeddable: child }); + } + await expect(check()).rejects.toThrow(Error); +}); diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.tsx b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.tsx new file mode 100644 index 00000000000000..537ac498a7dab2 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.tsx @@ -0,0 +1,93 @@ +/* + * 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 { prettyDuration, commonDurationRanges } from '@elastic/eui'; + +import { TimeRange } from '../../../../../../../src/plugins/data/public'; +import { + Action, + IEmbeddable, + ActionContext, + IncompatibleActionError, + Embeddable, + EmbeddableInput, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; + +import { CustomizeTimeRangeModal } from './customize_time_range_modal'; +import { doesInheritTimeRange } from './does_inherit_time_range'; +import { OpenModal, CommonlyUsedRange } from './types'; + +const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE'; + +export interface TimeRangeInput extends EmbeddableInput { + timeRange: TimeRange; +} + +function hasTimeRange( + embeddable: IEmbeddable | Embeddable +): embeddable is Embeddable { + return (embeddable as Embeddable).getInput().timeRange !== undefined; +} + +export class CustomTimeRangeBadge extends Action { + public readonly type = CUSTOM_TIME_RANGE_BADGE; + private openModal: OpenModal; + private dateFormat: string; + private commonlyUsedRanges: CommonlyUsedRange[]; + + constructor({ + openModal, + dateFormat, + commonlyUsedRanges, + }: { + openModal: OpenModal; + dateFormat: string; + commonlyUsedRanges: CommonlyUsedRange[]; + }) { + super(CUSTOM_TIME_RANGE_BADGE); + this.order = 7; + this.openModal = openModal; + this.dateFormat = dateFormat; + this.commonlyUsedRanges = commonlyUsedRanges; + } + + public getDisplayName({ embeddable }: ActionContext>) { + return prettyDuration( + embeddable.getInput().timeRange.from, + embeddable.getInput().timeRange.to, + commonDurationRanges, + this.dateFormat + ); + } + + public getIconType() { + return 'calendar'; + } + + public async isCompatible({ embeddable }: ActionContext) { + return Boolean(embeddable && hasTimeRange(embeddable) && !doesInheritTimeRange(embeddable)); + } + + public async execute({ embeddable }: ActionContext) { + const isCompatible = await this.isCompatible({ embeddable }); + if (!isCompatible) { + throw new IncompatibleActionError(); + } + + // Only here for typescript + if (hasTimeRange(embeddable)) { + const modalSession = this.openModal( + modalSession.close()} + embeddable={embeddable} + dateFormat={this.dateFormat} + commonlyUsedRanges={this.commonlyUsedRanges} + /> + ); + } + } +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/customize_time_range_modal.tsx b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/customize_time_range_modal.tsx new file mode 100644 index 00000000000000..dfa5768a4f8596 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/customize_time_range_modal.tsx @@ -0,0 +1,185 @@ +/* + * 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 } from 'react'; + +import { + EuiFormRow, + EuiButton, + EuiButtonEmpty, + EuiModalHeader, + EuiModalFooter, + EuiModalBody, + EuiModalHeaderTitle, + EuiSuperDatePicker, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { TimeRange } from '../../../../../../../src/plugins/data/public'; +import { + Embeddable, + IContainer, + ContainerInput, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TimeRangeInput } from './custom_time_range_action'; +import { doesInheritTimeRange } from './does_inherit_time_range'; +import { CommonlyUsedRange } from './types'; + +interface CustomizeTimeRangeProps { + embeddable: Embeddable; + onClose: () => void; + dateFormat?: string; + commonlyUsedRanges: CommonlyUsedRange[]; +} + +interface State { + timeRange?: TimeRange; + inheritTimeRange: boolean; +} + +export class CustomizeTimeRangeModal extends Component { + constructor(props: CustomizeTimeRangeProps) { + super(props); + this.state = { + timeRange: props.embeddable.getInput().timeRange, + inheritTimeRange: doesInheritTimeRange(props.embeddable), + }; + } + + onTimeChange = ({ start, end }: { start: string; end: string }) => { + this.setState({ timeRange: { from: start, to: end } }); + }; + + cancel = () => { + this.props.onClose(); + }; + + onInheritToggle = () => { + this.setState(prevState => ({ + inheritTimeRange: !prevState.inheritTimeRange, + })); + }; + + addToPanel = () => { + const { embeddable } = this.props; + + embeddable.updateInput({ timeRange: this.state.timeRange }); + + this.props.onClose(); + }; + + inheritFromParent = () => { + const { embeddable } = this.props; + const parent = embeddable.parent as IContainer<{}, ContainerInput>; + const parentPanels = parent!.getInput().panels; + + // Remove any explicit input to this child from the parent. + parent!.updateInput({ + panels: { + ...parentPanels, + [embeddable.id]: { + ...parentPanels[embeddable.id], + explicitInput: { + ...parentPanels[embeddable.id].explicitInput, + timeRange: undefined, + }, + }, + }, + }); + + this.props.onClose(); + }; + + public render() { + return ( + + + + {i18n.translate('xpack.advancedUiActions.customizeTimeRange.modal.headerTitle', { + defaultMessage: 'Customize panel time range', + })} + + + + + + { + return { + start: from, + end: to, + label: display, + }; + } + )} + /> + + + + + + + {i18n.translate( + 'xpack.advancedUiActions.customizePanelTimeRange.modal.removeButtonTitle', + { + defaultMessage: 'Remove', + } + )} + + + + + {i18n.translate( + 'xpack.advancedUiActions.customizePanelTimeRange.modal.cancelButtonTitle', + { + defaultMessage: 'Cancel', + } + )} + + + + + {this.state.inheritTimeRange + ? i18n.translate( + 'xpack.advancedUiActions.customizePanelTimeRange.modal.addToPanelButtonTitle', + { + defaultMessage: 'Add to panel', + } + ) + : i18n.translate( + 'xpack.advancedUiActions.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle', + { + defaultMessage: 'Update', + } + )} + + + + + + ); + } +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/does_inherit_time_range.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/does_inherit_time_range.ts new file mode 100644 index 00000000000000..aa960c45e20d67 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/does_inherit_time_range.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Embeddable, + IContainer, + ContainerInput, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TimeRangeInput } from './custom_time_range_action'; + +export function doesInheritTimeRange(embeddable: Embeddable) { + if (!embeddable.parent) { + return false; + } + + const parent = embeddable.parent as IContainer<{}, ContainerInput>; + + // Note: this logic might not work in a container nested world... the explicit input + // may be on the root... or any of the interim parents. + + // If there is no explicit input defined on the parent then this embeddable inherits the + // time range from whatever the time range of the parent is. + return parent.getInput().panels[embeddable.id].explicitInput.timeRange === undefined; +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/index.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/index.ts new file mode 100644 index 00000000000000..5dd807ba2442d7 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/index.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 { PluginInitializerContext } from 'src/core/public'; +import { AdvancedUiActionsPublicPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new AdvancedUiActionsPublicPlugin(initializerContext); +} + +export { AdvancedUiActionsPublicPlugin as Plugin }; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/legacy.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/legacy.ts new file mode 100644 index 00000000000000..18a2a4181040f4 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/legacy.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { npSetup, npStart } from 'ui/new_platform'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ + +import { plugin } from '.'; + +import { + setup as embeddableSetup, + start as embeddableStart, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; + +const pluginInstance = plugin({} as any); +export const setup = pluginInstance.setup(npSetup.core, { + embeddable: embeddableSetup, +}); +export const start = pluginInstance.start(npStart.core, { + embeddable: embeddableStart, +}); diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/plugin.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/plugin.ts new file mode 100644 index 00000000000000..afd3ccc93f21ce --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/plugin.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { + Plugin as EmbeddablePlugin, + CONTEXT_MENU_TRIGGER, + PANEL_BADGE_TRIGGER, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { CustomTimeRangeAction } from './custom_time_range_action'; + +import { CustomTimeRangeBadge } from './custom_time_range_badge'; +import { CommonlyUsedRange } from './types'; + +interface SetupDependencies { + embeddable: ReturnType; +} + +interface StartDependencies { + embeddable: ReturnType; +} + +export type Setup = void; +export type Start = void; + +export class AdvancedUiActionsPublicPlugin + implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, { embeddable }: SetupDependencies): Setup {} + + public start(core: CoreStart, { embeddable }: StartDependencies): Start { + const dateFormat = core.uiSettings.get('dateFormat') as string; + const commonlyUsedRanges = core.uiSettings.get('timepicker:quickRanges') as CommonlyUsedRange[]; + const timeRangeAction = new CustomTimeRangeAction({ + openModal: core.overlays.openModal, + dateFormat, + commonlyUsedRanges, + }); + embeddable.registerAction(timeRangeAction); + embeddable.attachAction(CONTEXT_MENU_TRIGGER, timeRangeAction.id); + + const timeRangeBadge = new CustomTimeRangeBadge({ + openModal: core.overlays.openModal, + dateFormat, + commonlyUsedRanges, + }); + embeddable.registerAction(timeRangeBadge); + embeddable.attachAction(PANEL_BADGE_TRIGGER, timeRangeBadge.id); + } + + public stop() {} +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/index.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/index.ts new file mode 100644 index 00000000000000..8d33b8344ff564 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/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 { TimeRangeEmbeddable, TIME_RANGE_EMBEDDABLE } from './time_range_embeddable'; +export { TimeRangeContainer } from './time_range_container'; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_container.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_container.ts new file mode 100644 index 00000000000000..0227e97fe35378 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_container.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimeRange } from '../../../../../../../../src/plugins/data/public'; +import { + ContainerInput, + Container, + ContainerOutput, + GetEmbeddableFactory, +} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; + +/** + * interfaces are not allowed to specify a sub-set of the required types until + * https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type + * here instead + */ +// eslint-disable-next-line @typescript-eslint/prefer-interface +export type InheritedChildrenInput = { + timeRange: TimeRange; + id?: string; +}; + +interface ContainerTimeRangeInput extends ContainerInput { + timeRange: TimeRange; +} + +const TIME_RANGE_CONTAINER = 'TIME_RANGE_CONTAINER'; + +export class TimeRangeContainer extends Container< + InheritedChildrenInput, + ContainerTimeRangeInput, + ContainerOutput +> { + public readonly type = TIME_RANGE_CONTAINER; + constructor( + initialInput: ContainerTimeRangeInput, + getFactory: GetEmbeddableFactory, + parent?: Container + ) { + super(initialInput, { embeddableLoaded: {} }, getFactory, parent); + } + + public getInheritedInput() { + return { timeRange: this.input.timeRange }; + } + + public render() {} + + public reload() {} +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable.ts new file mode 100644 index 00000000000000..7fb8c390f720c0 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable.ts @@ -0,0 +1,31 @@ +/* + * 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 { TimeRange } from '../../../../../../../../src/plugins/data/public'; +import { + EmbeddableOutput, + Embeddable, + EmbeddableInput, + IContainer, +} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; + +interface EmbeddableTimeRangeInput extends EmbeddableInput { + timeRange: TimeRange; +} + +export const TIME_RANGE_EMBEDDABLE = 'TIME_RANGE_EMBEDDABLE'; + +export class TimeRangeEmbeddable extends Embeddable { + public readonly type = TIME_RANGE_EMBEDDABLE; + + constructor(initialInput: EmbeddableTimeRangeInput, parent?: IContainer) { + super(initialInput, {}, parent); + } + + public render() {} + + public reload() {} +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable_factory.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable_factory.ts new file mode 100644 index 00000000000000..f4dcaa1e935ad9 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable_factory.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimeRange } from '../../../../../../../../src/plugins/data/public'; +import { + EmbeddableInput, + IContainer, + EmbeddableFactory, +} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TIME_RANGE_EMBEDDABLE, TimeRangeEmbeddable } from './time_range_embeddable'; + +interface EmbeddableTimeRangeInput extends EmbeddableInput { + timeRange: TimeRange; +} + +export class TimeRangeEmbeddableFactory extends EmbeddableFactory { + public readonly type = TIME_RANGE_EMBEDDABLE; + + public isEditable() { + return true; + } + + public async create(initialInput: EmbeddableTimeRangeInput, parent?: IContainer) { + return new TimeRangeEmbeddable(initialInput, parent); + } + + public getDisplayName() { + return 'time range'; + } +} diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/types.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/types.ts new file mode 100644 index 00000000000000..626782ba372ce4 --- /dev/null +++ b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OverlayRef } from 'src/core/public'; + +export interface CommonlyUsedRange { + from: string; + to: string; + display: string; +} + +export type OpenModal = ( + modalChildren: React.ReactNode, + modalProps?: { + closeButtonAriaLabel?: string; + 'data-test-subj'?: string; + } +) => OverlayRef; diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts index f14da9e9ed83d8..cb578442071be6 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts @@ -23,7 +23,7 @@ const alertTypeRegistryParams = { }; }, taskManager, - fireAction: jest.fn(), + executeAction: jest.fn(), internalSavedObjectsRepository: SavedObjectsClientMock.create(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), getBasePath: jest.fn().mockReturnValue(undefined), @@ -83,7 +83,7 @@ Object { expect(firstCall.internalSavedObjectsRepository).toBeTruthy(); expect(firstCall.getBasePath).toBeTruthy(); expect(firstCall.spaceIdToNamespace).toBeTruthy(); - expect(firstCall.fireAction).toMatchInlineSnapshot(`[MockFunction]`); + expect(firstCall.executeAction).toMatchInlineSnapshot(`[MockFunction]`); }); test('should throw an error if type is already registered', () => { diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts index 7e763858fd20e5..e5eda7ad8dd059 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts @@ -16,7 +16,7 @@ import { SpacesPlugin } from '../../spaces'; interface ConstructorOptions { getServices: (basePath: string) => Services; taskManager: TaskManager; - fireAction: ActionsPlugin['fire']; + executeAction: ActionsPlugin['execute']; internalSavedObjectsRepository: SavedObjectsClientContract; spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; getBasePath: SpacesPlugin['getBasePath']; @@ -25,7 +25,7 @@ interface ConstructorOptions { export class AlertTypeRegistry { private readonly getServices: (basePath: string) => Services; private readonly taskManager: TaskManager; - private readonly fireAction: ActionsPlugin['fire']; + private readonly executeAction: ActionsPlugin['execute']; private readonly alertTypes: Map = new Map(); private readonly internalSavedObjectsRepository: SavedObjectsClientContract; private readonly spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; @@ -33,14 +33,14 @@ export class AlertTypeRegistry { constructor({ internalSavedObjectsRepository, - fireAction, + executeAction, taskManager, getServices, spaceIdToNamespace, getBasePath, }: ConstructorOptions) { this.taskManager = taskManager; - this.fireAction = fireAction; + this.executeAction = executeAction; this.internalSavedObjectsRepository = internalSavedObjectsRepository; this.getServices = getServices; this.getBasePath = getBasePath; @@ -70,7 +70,7 @@ export class AlertTypeRegistry { createTaskRunner: getCreateTaskRunnerFunction({ alertType, getServices: this.getServices, - fireAction: this.fireAction, + executeAction: this.executeAction, internalSavedObjectsRepository: this.internalSavedObjectsRepository, getBasePath: this.getBasePath, spaceIdToNamespace: this.spaceIdToNamespace, diff --git a/x-pack/legacy/plugins/alerting/server/init.ts b/x-pack/legacy/plugins/alerting/server/init.ts index 947e98d0d93616..18efb9171883b5 100644 --- a/x-pack/legacy/plugins/alerting/server/init.ts +++ b/x-pack/legacy/plugins/alerting/server/init.ts @@ -51,7 +51,7 @@ export function init(server: Legacy.Server) { const alertTypeRegistry = new AlertTypeRegistry({ getServices, taskManager: taskManager!, - fireAction: server.plugins.actions!.fire, + executeAction: server.plugins.actions!.execute, internalSavedObjectsRepository: savedObjectsRepositoryWithInternalUser, getBasePath(...args) { return spaces.isEnabled diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts index bbafd60cdbc648..30b342d9e5cb35 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts @@ -7,7 +7,7 @@ import { createFireHandler } from './create_fire_handler'; const createFireHandlerParams = { - fireAction: jest.fn(), + executeAction: jest.fn(), spaceId: 'default', spaceIdToNamespace: jest.fn().mockReturnValue(undefined), getBasePath: jest.fn().mockReturnValue(undefined), @@ -44,11 +44,11 @@ const createFireHandlerParams = { beforeEach(() => jest.resetAllMocks()); -test('calls fireAction per selected action', async () => { +test('calls executeAction per selected action', async () => { const fireHandler = createFireHandler(createFireHandlerParams); await fireHandler('default', {}, {}); - expect(createFireHandlerParams.fireAction).toHaveBeenCalledTimes(1); - expect(createFireHandlerParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createFireHandlerParams.executeAction).toHaveBeenCalledTimes(1); + expect(createFireHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -63,17 +63,17 @@ test('calls fireAction per selected action', async () => { `); }); -test('limits fireAction per action group', async () => { +test('limits executeAction per action group', async () => { const fireHandler = createFireHandler(createFireHandlerParams); await fireHandler('other-group', {}, {}); - expect(createFireHandlerParams.fireAction).toMatchInlineSnapshot(`[MockFunction]`); + expect(createFireHandlerParams.executeAction).toMatchInlineSnapshot(`[MockFunction]`); }); test('context attribute gets parameterized', async () => { const fireHandler = createFireHandler(createFireHandlerParams); await fireHandler('default', { value: 'context-val' }, {}); - expect(createFireHandlerParams.fireAction).toHaveBeenCalledTimes(1); - expect(createFireHandlerParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createFireHandlerParams.executeAction).toHaveBeenCalledTimes(1); + expect(createFireHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -91,8 +91,8 @@ test('context attribute gets parameterized', async () => { test('state attribute gets parameterized', async () => { const fireHandler = createFireHandler(createFireHandlerParams); await fireHandler('default', {}, { value: 'state-val' }); - expect(createFireHandlerParams.fireAction).toHaveBeenCalledTimes(1); - expect(createFireHandlerParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createFireHandlerParams.executeAction).toHaveBeenCalledTimes(1); + expect(createFireHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -110,7 +110,7 @@ test('state attribute gets parameterized', async () => { test('throws error if reference not found', async () => { const params = { spaceId: 'default', - fireAction: jest.fn(), + executeAction: jest.fn(), alertSavedObject: { id: '1', type: 'alert', diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts index f51b374298a077..b14510343c88f9 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts @@ -10,13 +10,13 @@ import { ActionsPlugin } from '../../../actions'; import { transformActionParams } from './transform_action_params'; interface CreateFireHandlerOptions { - fireAction: ActionsPlugin['fire']; + executeAction: ActionsPlugin['execute']; alertSavedObject: SavedObject; spaceId: string; } export function createFireHandler({ - fireAction, + executeAction, alertSavedObject, spaceId, }: CreateFireHandlerOptions) { @@ -40,7 +40,7 @@ export function createFireHandler({ }; }); for (const action of actions) { - await fireAction({ + await executeAction({ id: action.id, params: action.params, spaceId, diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts index 95969d3d9a17ce..7d2387ec56875a 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts @@ -52,7 +52,7 @@ const getCreateTaskRunnerFunctionParams = { name: 'My test alert', executor: jest.fn(), }, - fireAction: jest.fn(), + executeAction: jest.fn(), internalSavedObjectsRepository: savedObjectsClient, spaceIdToNamespace: jest.fn().mockReturnValue(undefined), getBasePath: jest.fn().mockReturnValue(undefined), @@ -128,8 +128,8 @@ test('fireAction is called per alert instance that fired', async () => { savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); const runner = createTaskRunner({ taskInstance: mockedTaskInstance }); await runner.run(); - expect(getCreateTaskRunnerFunctionParams.fireAction).toHaveBeenCalledTimes(1); - expect(getCreateTaskRunnerFunctionParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(getCreateTaskRunnerFunctionParams.executeAction).toHaveBeenCalledTimes(1); + expect(getCreateTaskRunnerFunctionParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts index 2ac3023a2079cf..996271a26414ef 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts @@ -18,7 +18,7 @@ import { SpacesPlugin } from '../../../spaces'; interface CreateTaskRunnerFunctionOptions { getServices: (basePath: string) => Services; alertType: AlertType; - fireAction: ActionsPlugin['fire']; + executeAction: ActionsPlugin['execute']; internalSavedObjectsRepository: SavedObjectsClientContract; spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; getBasePath: SpacesPlugin['getBasePath']; @@ -31,7 +31,7 @@ interface TaskRunnerOptions { export function getCreateTaskRunnerFunction({ getServices, alertType, - fireAction, + executeAction, internalSavedObjectsRepository, spaceIdToNamespace, getBasePath, @@ -54,7 +54,7 @@ export function getCreateTaskRunnerFunction({ const fireHandler = createFireHandler({ alertSavedObject, - fireAction, + executeAction, spaceId: taskInstance.params.spaceId, }); const alertInstances: Record = {}; diff --git a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index a7c61d1c79da43..3ba2bd27677730 100644 --- a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Error CONTAINER_ID 1`] = `undefined`; + exports[`Error ERROR_CULPRIT 1`] = `"handleOopsie"`; exports[`Error ERROR_EXC_HANDLED 1`] = `undefined`; @@ -12,6 +14,8 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Error ERROR_PAGE_URL 1`] = `undefined`; +exports[`Error HOST_NAME 1`] = `"my hostname"`; + exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Error METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -42,6 +46,8 @@ exports[`Error OBSERVER_VERSION_MAJOR 1`] = `undefined`; exports[`Error PARENT_ID 1`] = `"parentId"`; +exports[`Error POD_NAME 1`] = `undefined`; + exports[`Error PROCESSOR_EVENT 1`] = `"error"`; exports[`Error SERVICE_AGENT_NAME 1`] = `"java"`; @@ -86,6 +92,8 @@ exports[`Error URL_FULL 1`] = `undefined`; exports[`Error USER_ID 1`] = `undefined`; +exports[`Span CONTAINER_ID 1`] = `undefined`; + exports[`Span ERROR_CULPRIT 1`] = `undefined`; exports[`Span ERROR_EXC_HANDLED 1`] = `undefined`; @@ -98,6 +106,8 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Span ERROR_PAGE_URL 1`] = `undefined`; +exports[`Span HOST_NAME 1`] = `undefined`; + exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Span METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -128,6 +138,8 @@ exports[`Span OBSERVER_VERSION_MAJOR 1`] = `undefined`; exports[`Span PARENT_ID 1`] = `"parentId"`; +exports[`Span POD_NAME 1`] = `undefined`; + exports[`Span PROCESSOR_EVENT 1`] = `"span"`; exports[`Span SERVICE_AGENT_NAME 1`] = `"java"`; @@ -172,6 +184,8 @@ exports[`Span URL_FULL 1`] = `undefined`; exports[`Span USER_ID 1`] = `undefined`; +exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`; + exports[`Transaction ERROR_CULPRIT 1`] = `undefined`; exports[`Transaction ERROR_EXC_HANDLED 1`] = `undefined`; @@ -184,6 +198,8 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`; +exports[`Transaction HOST_NAME 1`] = `"my hostname"`; + exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; exports[`Transaction METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -214,6 +230,8 @@ exports[`Transaction OBSERVER_VERSION_MAJOR 1`] = `undefined`; exports[`Transaction PARENT_ID 1`] = `"parentId"`; +exports[`Transaction POD_NAME 1`] = `undefined`; + exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`; exports[`Transaction SERVICE_AGENT_NAME 1`] = `"java"`; diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts index a5d057817893cc..365a9865b6e47e 100644 --- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts @@ -57,3 +57,7 @@ export const METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED = 'jvm.memory.non_heap.committed'; export const METRIC_JAVA_NON_HEAP_MEMORY_USED = 'jvm.memory.non_heap.used'; export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count'; + +export const HOST_NAME = 'host.hostname'; +export const CONTAINER_ID = 'container.id'; +export const POD_NAME = 'kubernetes.pod.name'; diff --git a/x-pack/legacy/plugins/apm/common/projections/errors.ts b/x-pack/legacy/plugins/apm/common/projections/errors.ts new file mode 100644 index 00000000000000..c3094f5cbb0b66 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/errors.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Setup } from '../../server/lib/helpers/setup_request'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + ERROR_GROUP_ID +} from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getErrorGroupsProjection({ + setup, + serviceName +}: { + setup: Setup; + serviceName: string; +}) { + const { start, end, uiFiltersES, config } = setup; + + return { + index: config.get('apm_oss.errorIndices'), + body: { + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'error' } }, + { range: rangeFilter(start, end) }, + ...uiFiltersES + ] + } + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID + } + } + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/metrics.ts b/x-pack/legacy/plugins/apm/common/projections/metrics.ts new file mode 100644 index 00000000000000..51e74e19afb1f4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/metrics.ts @@ -0,0 +1,37 @@ +/* + * 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 { Setup } from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getMetricsProjection({ + setup, + serviceName +}: { + setup: Setup; + serviceName: string; +}) { + const { start, end, uiFiltersES, config } = setup; + + return { + index: config.get('apm_oss.metricsIndices'), + body: { + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { + range: rangeFilter(start, end) + }, + ...uiFiltersES + ] + } + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/services.ts b/x-pack/legacy/plugins/apm/common/projections/services.ts new file mode 100644 index 00000000000000..ab72211f92aa71 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/services.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Setup } from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getServicesProjection({ setup }: { setup: Setup }) { + const { start, end, uiFiltersES, config } = setup; + + return { + index: [ + config.get('apm_oss.metricsIndices'), + config.get('apm_oss.errorIndices'), + config.get('apm_oss.transactionIndices') + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } + }, + { range: rangeFilter(start, end) }, + ...uiFiltersES + ] + } + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME + } + } + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts b/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts new file mode 100644 index 00000000000000..6f7be349b0cba7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { omit } from 'lodash'; +import { Setup } from '../../server/lib/helpers/setup_request'; +import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; +import { Options } from '../../server/lib/transaction_groups/fetcher'; +import { getTransactionsProjection } from './transactions'; +import { mergeProjection } from './util/merge_projection'; + +export function getTransactionGroupsProjection({ + setup, + options +}: { + setup: Setup; + options: Options; +}) { + const transactionsProjection = getTransactionsProjection({ + setup, + ...(omit(options, 'type') as Omit) + }); + + const bool = + options.type === 'top_traces' + ? { + must_not: [{ exists: { field: PARENT_ID } }] + } + : {}; + + return mergeProjection(transactionsProjection, { + body: { + query: { + bool + }, + aggs: { + transactions: { + terms: { + field: TRANSACTION_NAME + } + } + } + } + }); +} diff --git a/x-pack/legacy/plugins/apm/common/projections/transactions.ts b/x-pack/legacy/plugins/apm/common/projections/transactions.ts new file mode 100644 index 00000000000000..63abb0572df878 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/transactions.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Setup } from '../../server/lib/helpers/setup_request'; +import { + SERVICE_NAME, + TRANSACTION_TYPE, + PROCESSOR_EVENT, + TRANSACTION_NAME +} from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getTransactionsProjection({ + setup, + serviceName, + transactionName, + transactionType +}: { + setup: Setup; + serviceName?: string; + transactionName?: string; + transactionType?: string; +}) { + const { start, end, uiFiltersES, config } = setup; + + const transactionNameFilter = transactionName + ? [{ term: { [TRANSACTION_NAME]: transactionName } }] + : []; + const transactionTypeFilter = transactionType + ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] + : []; + const serviceNameFilter = serviceName + ? [{ term: { [SERVICE_NAME]: serviceName } }] + : []; + + const bool = { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + ...transactionNameFilter, + ...transactionTypeFilter, + ...serviceNameFilter, + ...uiFiltersES + ] + }; + + return { + index: config.get('apm_oss.transactionIndices'), + body: { + query: { + bool + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/typings.ts b/x-pack/legacy/plugins/apm/common/projections/typings.ts new file mode 100644 index 00000000000000..b49dba5e0a6fde --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/typings.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchParams } from 'elasticsearch'; + +export type Projection = Omit & { + body: { + query: any; + } & { + aggs?: { + [key: string]: { + terms: any; + }; + }; + }; +}; + +export enum PROJECTION { + SERVICES = 'services', + TRANSACTION_GROUPS = 'transactionGroups', + TRACES = 'traces', + TRANSACTIONS = 'transactions', + METRICS = 'metrics', + ERROR_GROUPS = 'errorGroups' +} diff --git a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.test.ts b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.test.ts new file mode 100644 index 00000000000000..ae1b7c552ab4e4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mergeProjection } from './index'; + +describe('mergeProjection', () => { + it('overrides arrays', () => { + expect( + mergeProjection( + { body: { query: { bool: { must: [{ terms: ['a'] }] } } } }, + { body: { query: { bool: { must: [{ term: 'b' }] } } } } + ) + ).toEqual({ + body: { + query: { + bool: { + must: [ + { + term: 'b' + } + ] + } + } + } + }); + }); + + it('merges plain objects', () => { + expect( + mergeProjection( + { body: { query: {}, aggs: { foo: { terms: { field: 'bar' } } } } }, + { + body: { + aggs: { foo: { aggs: { bar: { terms: { field: 'baz' } } } } } + } + } + ) + ).toEqual({ + body: { + query: {}, + aggs: { + foo: { + terms: { + field: 'bar' + }, + aggs: { + bar: { + terms: { + field: 'baz' + } + } + } + } + } + } + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts new file mode 100644 index 00000000000000..5b6b5b0b7f058d --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { merge, isPlainObject } from 'lodash'; +import { Projection } from '../../typings'; + +type PlainObject = Record; + +type DeepMerge = U extends PlainObject + ? (T extends PlainObject + ? (Omit & + { + [key in keyof U]: T extends { [k in key]: any } + ? DeepMerge + : U[key]; + }) + : U) + : U; + +export function mergeProjection( + target: T, + source: U +): DeepMerge { + return merge({}, target, source, (a, b) => { + if (isPlainObject(a) && isPlainObject(b)) { + return undefined; + } + return b; + }) as DeepMerge; +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 4d5a87faa46a6b..57b7ea200ece70 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -12,7 +12,7 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useFetcher } from '../../../hooks/useFetcher'; import { loadErrorDistribution, @@ -22,12 +22,13 @@ import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; const ErrorGroupOverview: React.SFC = () => { - const { - urlParams: { serviceName, start, end, sortField, sortDirection }, - uiFilters - } = useUrlParams(); + const { urlParams, uiFilters } = useUrlParams(); + + const { serviceName, start, end, sortField, sortDirection } = urlParams; const { data: errorDistributionData } = useFetcher(() => { if (serviceName && start && end) { @@ -53,42 +54,62 @@ const ErrorGroupOverview: React.SFC = () => { } }, [serviceName, start, end, sortField, sortDirection, uiFilters]); - useTrackPageview({ app: 'apm', path: 'error_group_overview' }); + useTrackPageview({ + app: 'apm', + path: 'error_group_overview' + }); useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionResult', 'host', 'containerId', 'podName'], + params: { + serviceName + }, + projection: PROJECTION.ERROR_GROUPS + }; + + return config; + }, [serviceName]); + if (!errorDistributionData || !errorGroupListData) { return null; } return ( - - - - - - - - + + + + + + + + + + + + - - - - -

Errors

-
- -
-
+ + +

Errors

+
+ + + +
+ + ); }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 4a5f4c53d6e436..a44d1b83133253 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -32,7 +32,7 @@ export function ServiceDetailTabs({ urlParams }: Props) { defaultMessage: 'Transactions' }), path: `/services/${serviceName}/transactions`, - render: () => , + render: () => , name: 'transactions' }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 5e76140ce21e83..871e48c290503a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -4,12 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import { + EuiFlexGrid, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiFlexGroup +} from '@elastic/eui'; +import React, { useMemo } from 'react'; import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from './MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { agentName: string; @@ -17,22 +25,43 @@ interface ServiceMetricsProps { export function ServiceMetrics({ agentName }: ServiceMetricsProps) { const { urlParams } = useUrlParams(); + const { serviceName } = urlParams; const { data } = useServiceMetricCharts(urlParams, agentName); const { start, end } = urlParams; + + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( + () => ({ + filterNames: ['host', 'containerId', 'podName'], + params: { + serviceName + }, + projection: PROJECTION.METRICS, + showCount: false + }), + [serviceName] + ); + return ( - - - - {data.charts.map(chart => ( - - - - - - ))} - - - - + + + + + + + + {data.charts.map(chart => ( + + + + + + ))} + + + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index 013d803e6411d8..58b9ae2d868d08 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -13,6 +13,8 @@ import { ServiceOverview } from '..'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; import * as coreHooks from '../../../../hooks/useCore'; import { InternalCoreStart } from 'src/core/public'; +import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; jest.mock('ui/kfetch'); @@ -38,6 +40,13 @@ describe('Service Overview -> View', () => { } }); spyOn(coreHooks, 'useCore').and.returnValue(coreMock); + + jest.spyOn(useLocalUIFilters, 'useLocalUIFilters').mockReturnValue({ + filters: [], + setFilterValue: () => null, + clearValues: () => null, + status: FETCH_STATUS.SUCCESS + }); }); afterEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx index 78b6447a4ff19a..dd0937da477192 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { toastNotifications } from 'ui/notify'; import url from 'url'; import { useFetcher } from '../../../hooks/useFetcher'; @@ -17,6 +17,8 @@ import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; import { useCore } from '../../../hooks/useCore'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; const initalData = { items: [], @@ -75,17 +77,34 @@ export function ServiceOverview() { useTrackPageview({ app: 'apm', path: 'services_overview' }); useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( + () => ({ + filterNames: ['host', 'agentName'], + projection: PROJECTION.SERVICES + }), + [] + ); + return ( - - + + + + + + + } /> - } - /> - + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx index 2871efca018a18..74819600d44a9a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; -import React from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { loadTraceList } from '../../../services/rest/apm/traces'; import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; export function TraceOverview() { const { urlParams, uiFilters } = useUrlParams(); @@ -24,9 +26,25 @@ export function TraceOverview() { useTrackPageview({ app: 'apm', path: 'traces_overview' }); useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 }); + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionResult', 'host', 'containerId', 'podName'], + projection: PROJECTION.TRACES + }; + + return config; + }, []); + return ( - - - + + + + + + + + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx index f242690beab038..03321397b2da2c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -4,9 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiSpacer, EuiTitle, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiPanel, + EuiSpacer, + EuiTitle, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem +} from '@elastic/eui'; import _ from 'lodash'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; import { useWaterfall } from '../../../hooks/useWaterfall'; @@ -20,6 +27,8 @@ import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../infra/public'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; export function TransactionDetails() { const location = useLocation(); @@ -34,11 +43,24 @@ export function TransactionDetails() { const { data: waterfall, exceedsMax } = useWaterfall(urlParams); const transaction = waterfall.getTransactionById(urlParams.transactionId); - const { transactionName } = urlParams; + const { transactionName, transactionType, serviceName } = urlParams; useTrackPageview({ app: 'apm', path: 'transaction_details' }); useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 }); + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionResult'], + projection: PROJECTION.TRANSACTIONS, + params: { + transactionName, + transactionType, + serviceName + } + }; + return config; + }, [transactionName, transactionType, serviceName]); + return (
@@ -47,43 +69,50 @@ export function TransactionDetails() { - - + + + + + + + - + - - + + - + - - - + + + - + - {transaction && ( - - )} + {transaction && ( + + )} + +
); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx index 7b1770dd4745be..9bc8101c789838 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx @@ -5,12 +5,26 @@ */ import React from 'react'; -import { queryByLabelText, render } from 'react-testing-library'; -import { TransactionOverview } from '..'; -import * as useLocationHook from '../../../../hooks/useLocation'; +import { + queryByLabelText, + render, + queryBySelectText, + getByText, + getByDisplayValue, + queryByDisplayValue, + fireEvent +} from 'react-testing-library'; +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 { fromQuery } from '../../../shared/Links/url_helpers'; +import { Router } from 'react-router-dom'; +import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; + +jest.spyOn(history, 'push'); +jest.spyOn(history, 'replace'); jest.mock('ui/kfetch'); @@ -31,31 +45,34 @@ function setup({ urlParams: IUrlParams; serviceTransactionTypes: string[]; }) { - jest.spyOn(history, 'replace'); - jest - .spyOn(useLocationHook, 'useLocation') - .mockReturnValue({ pathname: '' } as any); + const defaultLocation = { + pathname: '/services/foo/transactions', + search: fromQuery(omit(urlParams, 'serviceName')) + } as any; + + history.replace({ + ...defaultLocation + }); jest .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') .mockReturnValue(serviceTransactionTypes); - const { container } = render(); - return { container }; + return render( + + + + + + ); } describe('TransactionOverview', () => { - describe('when no transaction type is given', () => { - it('should render null', () => { - const { container } = setup({ - serviceTransactionTypes: ['firstType', 'secondType'], - urlParams: { - serviceName: 'MyServiceName' - } - }); - expect(container).toMatchInlineSnapshot(`
`); - }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('when no transaction type is given', () => { it('should redirect to first type', () => { setup({ serviceTransactionTypes: ['firstType', 'secondType'], @@ -71,7 +88,7 @@ describe('TransactionOverview', () => { }); }); - const FILTER_BY_TYPE_LABEL = 'Filter by type'; + 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', () => { @@ -83,9 +100,33 @@ describe('TransactionOverview', () => { } }); - expect( - queryByLabelText(container, FILTER_BY_TYPE_LABEL) - ).toMatchSnapshot(); + // secondType is selected in the dropdown + expect(queryBySelectText(container, 'secondType')).not.toBeNull(); + expect(queryBySelectText(container, 'firstType')).toBeNull(); + + expect(getByText(container, 'firstType')).not.toBeNull(); + }); + + it('should update the URL when a transaction type is selected', () => { + const { container } = setup({ + serviceTransactionTypes: ['firstType', 'secondType'], + urlParams: { + transactionType: 'secondType', + serviceName: 'MyServiceName' + } + }); + + expect(queryByDisplayValue(container, 'firstType')).toBeNull(); + + fireEvent.change(getByDisplayValue(container, 'secondType'), { + target: { value: 'firstType' } + }); + + expect(history.push).toHaveBeenCalled(); + + getByDisplayValue(container, 'firstType'); + + expect(queryByDisplayValue(container, 'firstType')).not.toBeNull(); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/__snapshots__/TransactionOverview.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/__snapshots__/TransactionOverview.test.tsx.snap deleted file mode 100644 index 034e39de54da71..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/__snapshots__/TransactionOverview.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TransactionOverview when transactionType is selected and multiple transaction types are given should render dropdown with transaction types 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx index e2244e7ea1087f..52450bc3876f32 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -5,16 +5,16 @@ */ import { - EuiFormRow, EuiPanel, - EuiSelect, EuiSpacer, - EuiTitle + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import { first } from 'lodash'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; @@ -29,11 +29,11 @@ import { useLocation } from '../../../hooks/useLocation'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../infra/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; - -interface Props { - urlParams: IUrlParams; -} +import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; function getRedirectLocation({ urlParams, @@ -58,8 +58,9 @@ function getRedirectLocation({ } } -export function TransactionOverview({ urlParams }: Props) { +export function TransactionOverview() { const location = useLocation(); + const { urlParams } = useUrlParams(); const { serviceName, transactionType } = urlParams; // TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context? @@ -90,6 +91,20 @@ export function TransactionOverview({ urlParams }: Props) { } }, [serviceName, transactionType]); + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( + () => ({ + filterNames: ['transactionResult', 'host', 'containerId', 'podName'], + params: { + serviceName, + transactionType + }, + projection: PROJECTION.TRANSACTION_GROUPS + }), + [serviceName, transactionType] + ); + // TODO: improve urlParams typings. // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed if (!serviceName || !transactionType) { @@ -97,63 +112,41 @@ export function TransactionOverview({ urlParams }: Props) { } return ( - - {/* TODO: This should be replaced by local filters */} - {serviceTransactionTypes.length > 1 ? ( - - ({ - text: `${type}`, - value: type - }))} - value={transactionType} - onChange={event => { - history.push({ - ...location, - pathname: `/services/${urlParams.serviceName}/transactions`, - search: fromQuery({ - ...toQuery(location.search), - transactionType: event.target.value - }) - }); - }} + + + + + + + + + + + + + + + - - ) : null} - - - + - - - - - - - -

Transactions

-
- - -
-
+ + +

Transactions

+
+ + +
+ + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx index f1eac0a08142ea..78fa47e87a5986 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx @@ -8,6 +8,7 @@ import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import React from 'react'; import { matchPath, Route, RouteComponentProps } from 'react-router-dom'; import { omit } from 'lodash'; +import { localUIFilterNames } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { useLocation } from '../../../hooks/useLocation'; import { history } from '../../../utils/history'; import { toQuery, fromQuery } from '../Links/url_helpers'; @@ -39,17 +40,18 @@ export function HistoryTabs({ tabs }: HistoryTabsProps) { {tabs.map((tab, i) => ( { - const searchWithoutTableParameters = omit( + const persistedQueryParameters = omit( toQuery(location.search), 'sortField', 'sortDirection', 'page', - 'pageSize' + 'pageSize', + ...localUIFilterNames ); history.push({ ...location, pathname: tab.path, - search: fromQuery(searchWithoutTableParameters) + search: fromQuery(persistedQueryParameters) }); }} isSelected={isTabSelected(tab, location.pathname)} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts index 0575c0837668c8..763ee93df64156 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -5,6 +5,7 @@ */ import qs from 'querystring'; +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { StringMap } from '../../../../typings/common'; export function toQuery(search?: string): APMQueryParamsRaw { @@ -19,7 +20,7 @@ export function fromQuery(query: StringMap) { }); } -export interface APMQueryParams { +export type APMQueryParams = { transactionId?: string; transactionName?: string; transactionType?: string; @@ -38,7 +39,7 @@ export interface APMQueryParams { rangeTo?: string; refreshPaused?: string | boolean; refreshInterval?: string | number; -} +} & { [key in LocalUIFilterName]?: string }; // forces every value of T[K] to be type: string type StringifyAll = { [K in keyof T]: string }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx new file mode 100644 index 00000000000000..a1140dcbbb3fcf --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGrid, EuiFlexItem, EuiBadge, EuiIcon } from '@elastic/eui'; +import styled from 'styled-components'; +import { unit, px, truncate } from '../../../../style/variables'; + +const BadgeText = styled.div` + display: inline-block; + ${truncate(px(unit * 8))}; + vertical-align: middle; +`; + +interface Props { + value: string[]; + onRemove: (val: string) => void; +} + +const FilterBadgeList = ({ onRemove, value }: Props) => ( + + {value.map(val => ( + + + + ))} + +); + +export { FilterBadgeList }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx new file mode 100644 index 00000000000000..a257e52ab01cc5 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx @@ -0,0 +1,35 @@ +/* + * 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 { EuiButtonEmpty, EuiTitle } from '@elastic/eui'; +import styled from 'styled-components'; + +const Button = styled(EuiButtonEmpty).attrs({ + contentProps: { + className: 'alignLeft' + }, + color: 'text' +})` + width: 100%; + + .alignLeft { + justify-content: flex-start; + padding-left: 0; + } +`; + +type Props = React.ComponentProps; + +export const FilterTitleButton = (props: Props) => { + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx new file mode 100644 index 00000000000000..9f13ee3fbe59d7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx @@ -0,0 +1,173 @@ +/* + * 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, { useState, useMemo, useEffect } from 'react'; +import { + EuiTitle, + EuiPopover, + EuiSelectable, + EuiSpacer, + EuiHorizontalRule, + EuiText, + EuiButton, + EuiFlexItem, + EuiFlexGroup +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import styled from 'styled-components'; +import { FilterBadgeList } from './FilterBadgeList'; +import { unit, px } from '../../../../style/variables'; +import { FilterTitleButton } from './FilterTitleButton'; + +const Popover = styled(EuiPopover).attrs({ + anchorClassName: 'anchor' +})` + .anchor { + display: block; + } +`; + +const SelectContainer = styled.div` + width: ${px(unit * 16)}; +`; + +const Counter = styled.div` + border-radius: ${theme.euiBorderRadius}; + background: ${theme.euiColorLightShade}; + padding: 0 ${theme.paddingSizes.xs}; +`; + +const ApplyButton = styled(EuiButton)` + align-self: flex-end; +`; + +interface Props { + name: string; + title: string; + options: Array<{ + name: string; + count: number; + }>; + onChange: (value: string[]) => void; + value: string[]; + showCount: boolean; +} + +type Option = EuiSelectable['props']['options'][0]; + +const Filter = ({ + name, + title, + options, + onChange, + value, + showCount +}: Props) => { + const [showPopover, setShowPopover] = useState(false); + + const toggleShowPopover = () => setShowPopover(show => !show); + + const button = ( + {title} + ); + + const items: Option[] = useMemo( + () => + options.map(option => ({ + label: option.name, + append: showCount ? ( + + {option.count} + + ) : null, + checked: value.includes(option.name) ? 'on' : undefined + })), + [value, options, showCount] + ); + + const [visibleOptions, setVisibleOptions] = useState(items); + + useEffect(() => { + setVisibleOptions(items); + }, [items]); + + return ( + <> + + { + setVisibleOptions(selectedOptions); + }} + options={visibleOptions} + searchable={true} + > + {(list, search) => ( + + + + +

+ {i18n.translate('xpack.apm.applyFilter', { + defaultMessage: 'Apply {title} filter', + values: { title } + })} +

+
+ + + {search} + + {list} + +
+ + { + const newValue = visibleOptions + .filter(option => option.checked === 'on') + .map(option => option.label); + + setShowPopover(false); + onChange(newValue); + }} + size="s" + > + {i18n.translate('xpack.apm.applyOptions', { + defaultMessage: 'Apply options' + })} + + +
+
+ )} +
+
+ {value.length ? ( + <> + { + onChange(value.filter(v => val !== v)); + }} + value={value} + /> + + + ) : null} + + ); +}; + +export { Filter }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx new file mode 100644 index 00000000000000..f043da824d3757 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx @@ -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 React from 'react'; +import { + EuiTitle, + EuiHorizontalRule, + EuiSpacer, + EuiSelect +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../Links/url_helpers'; + +interface Props { + transactionTypes: string[]; +} + +const TransactionTypeFilter = ({ transactionTypes }: Props) => { + const { + urlParams: { transactionType } + } = useUrlParams(); + + const options = transactionTypes.map(type => ({ + text: type, + value: type + })); + + return ( + <> + +

+ {i18n.translate('xpack.apm.localFilters.titles.transactionType', { + defaultMessage: 'Transaction type' + })} +

+
+ + + + { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + transactionType: event.target.value + }) + }; + history.push(newLocation); + }} + /> + + ); +}; + +export { TransactionTypeFilter }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx new file mode 100644 index 00000000000000..ef05c21224c694 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -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 React from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiHorizontalRule, + EuiButtonEmpty +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; +import { Filter } from './Filter'; +import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; + +interface Props { + projection: PROJECTION; + filterNames: LocalUIFilterName[]; + params?: Record; + showCount?: boolean; + children?: React.ReactNode; +} + +const ButtonWrapper = styled.div` + display: inline-block; +`; + +const LocalUIFilters = ({ + projection, + params, + filterNames, + children, + showCount = true +}: Props) => { + const { filters, setFilterValue, clearValues } = useLocalUIFilters({ + filterNames, + projection, + params + }); + + return ( + <> + +

+ {i18n.translate('xpack.apm.localFiltersTitle', { + defaultMessage: 'Filters' + })} +

+
+ + {children} + {filters.map(filter => { + return ( + + { + setFilterValue(filter.name, value); + }} + showCount={showCount} + /> + + + ); + })} + + + + {i18n.translate('xpack.apm.clearFilters', { + defaultMessage: 'Clear filters' + })} + + + + ); +}; + +export { LocalUIFilters }; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx index 8cf326d6cfd6ae..a26c2797135d3d 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx @@ -12,19 +12,34 @@ import React, { useState } from 'react'; import { withRouter } from 'react-router-dom'; -import { uniqueId } from 'lodash'; +import { uniqueId, mapValues } from 'lodash'; import { IUrlParams } from './types'; import { getParsedDate } from './helpers'; import { resolveUrlParams } from './resolveUrlParams'; import { UIFilters } from '../../../typings/ui-filters'; +import { + localUIFilterNames, + LocalUIFilterName +} from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../utils/pickKeys'; interface TimeRange { rangeFrom: string; rangeTo: string; } -function useUiFilters({ kuery, environment }: IUrlParams): UIFilters { - return useMemo(() => ({ kuery, environment }), [kuery, environment]); +function useUiFilters( + params: Pick +): UIFilters { + return useMemo(() => { + const { kuery, environment, ...localUIFilters } = params; + const mappedLocalFilters = mapValues( + pickKeys(localUIFilters, ...localUIFilterNames), + val => (val ? val.split(',') : []) + ) as Partial>; + + return { kuery, environment, ...mappedLocalFilters }; + }, [params]); } const defaultRefresh = (time: TimeRange) => {}; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index 234bbc55a10699..06abf9c0945b75 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -17,6 +17,8 @@ import { } from './helpers'; import { toQuery } from '../../components/shared/Links/url_helpers'; import { TIMEPICKER_DEFAULTS } from './constants'; +import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../utils/pickKeys'; type TimeUrlParams = Pick< IUrlParams, @@ -28,6 +30,8 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { location.pathname ); + const query = toQuery(location.search); + const { traceId, transactionId, @@ -47,7 +51,9 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { rangeFrom = TIMEPICKER_DEFAULTS.rangeFrom, rangeTo = TIMEPICKER_DEFAULTS.rangeTo, environment - } = toQuery(location.search); + } = query; + + const localUIFilters = pickKeys(query, ...localUIFilterNames); return removeUndefinedProps({ // date params @@ -79,6 +85,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { errorGroupId, // ui filters - environment + environment, + ...localUIFilters }); } diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts index bb4af42b7a73a8..979fd3e8d60bdd 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IUrlParams { +import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; + +export type IUrlParams = { detailTab?: string; end?: string; errorGroupId?: string; @@ -26,4 +28,4 @@ export interface IUrlParams { waterfallItemId?: string; page?: number; pageSize?: number; -} +} & Partial>; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts new file mode 100644 index 00000000000000..4158c37c2a8237 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { omit } from 'lodash'; +import { useFetcher } from './useFetcher'; +import { callApi } from '../services/rest/callApi'; +import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; +import { useUrlParams } from './useUrlParams'; +import { LocalUIFilterName } from '../../server/lib/ui_filters/local_ui_filters/config'; +import { history } from '../utils/history'; +import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; +import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; +import { PROJECTION } from '../../common/projections/typings'; +import { pickKeys } from '../utils/pickKeys'; + +const initialData = [] as LocalUIFiltersAPIResponse; + +export function useLocalUIFilters({ + projection, + filterNames, + params +}: { + projection: PROJECTION; + filterNames: LocalUIFilterName[]; + params?: Record; +}) { + const { uiFilters, urlParams } = useUrlParams(); + + const values = pickKeys(uiFilters, ...filterNames); + + const setFilterValue = (name: LocalUIFilterName, value: string[]) => { + const search = omit(toQuery(history.location.search), name); + + history.push({ + ...history.location, + search: fromQuery( + removeUndefinedProps({ + ...search, + [name]: value.length ? value.join(',') : undefined + }) + ) + }); + }; + + const clearValues = () => { + const search = omit(toQuery(history.location.search), filterNames); + history.push({ + ...history.location, + search: fromQuery(search) + }); + }; + + const { data = initialData, status } = useFetcher(async () => { + const foo = await callApi({ + method: 'GET', + pathname: `/api/apm/ui_filters/local_filters/${projection}`, + query: { + uiFilters: JSON.stringify(uiFilters), + start: urlParams.start, + end: urlParams.end, + filterNames: JSON.stringify(filterNames), + ...params + } + }); + return foo; + }, [uiFilters, urlParams, params, filterNames, projection]); + + const filters = data.map(filter => ({ + ...filter, + value: values[filter.name] || [] + })); + + return { + filters, + status, + setFilterValue, + clearValues + }; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts b/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts new file mode 100644 index 00000000000000..f3ab62d33cf550 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { pick } from 'lodash'; + +export function pickKeys(obj: T, ...keys: K[]) { + return pick(obj, keys) as Pick; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index bfd6db2e92b559..2a772f4300df4f 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -15,7 +15,9 @@ import { Moment } from 'moment-timezone'; import React from 'react'; import { render, waitForElement } from 'react-testing-library'; import { MemoryRouter } from 'react-router-dom'; +import { ESFilter } from 'elasticsearch'; import { LocationProvider } from '../context/LocationContext'; +import { PromiseReturnType } from '../../typings/common'; export function toJson(wrapper: ReactWrapper) { return enzymeToJson(wrapper, { @@ -90,3 +92,53 @@ export function expectTextsInDocument(output: any, texts: string[]) { expect(output.getByText(text)).toBeInTheDocument(); }); } + +interface MockSetup { + start: number; + end: number; + client: any; + config: { + get: any; + has: any; + }; + uiFiltersES: ESFilter[]; +} + +export async function inspectSearchParams( + fn: (mockSetup: MockSetup) => Promise +) { + const clientSpy = jest.fn().mockReturnValueOnce({ + hits: { + total: 0 + } + }); + + const mockSetup = { + start: 1528113600000, + end: 1528977600000, + client: { + search: clientSpy + } as any, + config: { + get: () => 'myIndex' as any, + has: () => true + }, + uiFiltersES: [ + { + term: { 'service.environment': 'prod' } + } + ] + }; + try { + await fn(mockSetup); + } catch { + // we're only extracting the search params + } + + return { + params: clientSpy.mock.calls[0][0], + teardown: () => clientSpy.mockClear() + }; +} + +export type SearchParamsMock = PromiseReturnType; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..76b2d67199bf73 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -0,0 +1,248 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`error queries fetches a single error group 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "term": Object { + "error.grouping_key": "groupId", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "size": 1, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + "index": "myIndex", +} +`; + +exports[`error queries fetches multiple error groups 1`] = ` +Object { + "body": Object { + "aggs": Object { + "error_groups": Object { + "aggs": Object { + "sample": Object { + "top_hits": Object { + "_source": Array [ + "error.log.message", + "error.exception.message", + "error.exception.handled", + "error.culprit", + "error.grouping_key", + "@timestamp", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": "desc", + }, + ], + }, + }, + }, + "terms": Object { + "field": "error.grouping_key", + "order": Object { + "_count": "asc", + }, + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = ` +Object { + "body": Object { + "aggs": Object { + "error_groups": Object { + "aggs": Object { + "max_timestamp": Object { + "max": Object { + "field": "@timestamp", + }, + }, + "sample": Object { + "top_hits": Object { + "_source": Array [ + "error.log.message", + "error.exception.message", + "error.exception.handled", + "error.culprit", + "error.grouping_key", + "@timestamp", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": "desc", + }, + ], + }, + }, + }, + "terms": Object { + "field": "error.grouping_key", + "order": Object { + "max_timestamp": "asc", + }, + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`error queries fetches trace errors 1`] = ` +Object { + "body": Object { + "aggs": Object { + "transactions": Object { + "terms": Object { + "field": "transaction.id", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "trace.id": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..e1d7e9843bf697 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`error distribution queries fetches an error distribution 1`] = ` +Object { + "body": Object { + "aggs": Object { + "distribution": Object { + "histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "interval": NaN, + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`error distribution queries fetches an error distribution with a group id 1`] = ` +Object { + "body": Object { + "aggs": Object { + "distribution": Object { + "histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "interval": NaN, + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "error.grouping_key": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/queries.test.ts new file mode 100644 index 00000000000000..fcc456c6533038 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/queries.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getErrorDistribution } from './get_distribution'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../../public/utils/testHelpers'; + +describe('error distribution queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches an error distribution', async () => { + mock = await inspectSearchParams(setup => + getErrorDistribution({ + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches an error distribution with a group id', async () => { + mock = await inspectSearchParams(setup => + getErrorDistribution({ + serviceName: 'serviceName', + groupId: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts index 33a93fa986db30..df37568c770fca 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts @@ -10,14 +10,13 @@ import { ERROR_EXC_HANDLED, ERROR_EXC_MESSAGE, ERROR_GROUP_ID, - ERROR_LOG_MESSAGE, - PROCESSOR_EVENT, - SERVICE_NAME + ERROR_LOG_MESSAGE } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; import { APMError } from '../../../typings/es_schemas/ui/APMError'; -import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; +import { getErrorGroupsProjection } from '../../../common/projections/errors'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; export type ErrorGroupListAPIResponse = PromiseReturnType< typeof getErrorGroups @@ -34,29 +33,19 @@ export async function getErrorGroups({ sortDirection: string; setup: Setup; }) { - const { start, end, uiFiltersES, client, config } = setup; + const { client } = setup; // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; - const params = { - index: config.get('apm_oss.errorIndices'), + const projection = getErrorGroupsProjection({ setup, serviceName }); + + const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, - { range: rangeFilter(start, end) }, - ...uiFiltersES - ] - } - }, aggs: { error_groups: { terms: { - field: ERROR_GROUP_ID, size: 500, order: sortByLatestOccurrence ? { @@ -92,7 +81,7 @@ export async function getErrorGroups({ } } } - }; + }); interface SampleError { '@timestamp': APMError['@timestamp']; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts new file mode 100644 index 00000000000000..2b1704d9424e47 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { getErrorGroup } from './get_error_group'; +import { getErrorGroups } from './get_error_groups'; +import { getTraceErrorsPerTransaction } from './get_trace_errors_per_transaction'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('error queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches a single error group', async () => { + mock = await inspectSearchParams(setup => + getErrorGroup({ + groupId: 'groupId', + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches multiple error groups', async () => { + mock = await inspectSearchParams(setup => + getErrorGroups({ + sortDirection: 'asc', + sortField: 'foo', + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches multiple error groups when sortField = latestOccurrenceAt', async () => { + mock = await inspectSearchParams(setup => + getErrorGroups({ + sortDirection: 'asc', + sortField: 'latestOccurrenceAt', + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches trace errors', async () => { + mock = await inspectSearchParams(setup => + getTraceErrorsPerTransaction('foo', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts index 8a27799cdfe68a..1658cb07e4d2f6 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts @@ -9,12 +9,33 @@ import { ESFilter } from 'elasticsearch'; import { UIFilters } from '../../../../typings/ui-filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; import { getKueryUiFilterES } from './get_kuery_ui_filter_es'; +import { + localUIFilters, + localUIFilterNames +} from '../../ui_filters/local_ui_filters/config'; export async function getUiFiltersES(server: Server, uiFilters: UIFilters) { - const kuery = await getKueryUiFilterES(server, uiFilters.kuery); - const environment = getEnvironmentUiFilterES(uiFilters.environment); + const { kuery, environment, ...localFilterValues } = uiFilters; + + const mappedFilters = localUIFilterNames + .filter(name => name in localFilterValues) + .map(filterName => { + const field = localUIFilters[filterName]; + const value = localFilterValues[filterName]; + return { + terms: { + [field.fieldName]: value + } + }; + }) as ESFilter[]; // remove undefined items from list - const filters = [kuery, environment].filter(filter => !!filter) as ESFilter[]; - return filters; + const esFilters = [ + await getKueryUiFilterES(server, uiFilters.kuery), + getEnvironmentUiFilterES(uiFilters.environment) + ] + .filter(filter => !!filter) + .concat(mappedFilters) as ESFilter[]; + + return esFilters; } diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..16d03c0b504989 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -0,0 +1,448 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`metrics queries fetches cpu chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "processCPUAverage": Object { + "avg": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "processCPUMax": Object { + "max": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "systemCPUAverage": Object { + "avg": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + "systemCPUMax": Object { + "max": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "processCPUAverage": Object { + "avg": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "processCPUMax": Object { + "max": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "systemCPUAverage": Object { + "avg": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + "systemCPUMax": Object { + "max": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches heap memory chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "heapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.heap.committed", + }, + }, + "heapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.heap.max", + }, + }, + "heapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.heap.used", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "heapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.heap.committed", + }, + }, + "heapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.heap.max", + }, + }, + "heapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.heap.used", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "agent.name": "java", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches memory chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "memoryUsedAvg": Object { + "avg": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + "memoryUsedMax": Object { + "max": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + "timeseriesData": Object { + "aggs": Object { + "memoryUsedAvg": Object { + "avg": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + "memoryUsedMax": Object { + "max": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "exists": Object { + "field": "system.memory.actual.free", + }, + }, + Object { + "exists": Object { + "field": "system.memory.total", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches non heap memory chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "nonHeapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.non_heap.committed", + }, + }, + "nonHeapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.non_heap.max", + }, + }, + "nonHeapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.non_heap.used", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "nonHeapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.non_heap.committed", + }, + }, + "nonHeapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.non_heap.max", + }, + }, + "nonHeapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.non_heap.used", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "agent.name": "java", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches thread count chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "threadCount": Object { + "avg": Object { + "field": "jvm.thread.count", + }, + }, + "threadCountMax": Object { + "max": Object { + "field": "jvm.thread.count", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "threadCount": Object { + "avg": Object { + "field": "jvm.thread.count", + }, + }, + "threadCountMax": Object { + "max": Object { + "field": "jvm.thread.count", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "agent.name": "java", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index eefa1e0ef201a3..89b263612359e2 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - PROCESSOR_EVENT, - SERVICE_NAME -} from '../../../common/elasticsearch_fieldnames'; import { Setup } from '../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../helpers/metrics'; -import { rangeFilter } from '../helpers/range_filter'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; +import { getMetricsProjection } from '../../../common/projections/metrics'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; interface Aggs { [key: string]: { @@ -45,23 +42,16 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, uiFiltersES, client, config } = setup; + const { start, end, client } = setup; - const params = { - index: config.get('apm_oss.metricsIndices'), + const projection = getMetricsProjection({ setup, serviceName }); + + const params = mergeProjection(projection, { body: { size: 0, query: { bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { - range: rangeFilter(start, end) - }, - ...additionalFilters, - ...uiFiltersES - ] + filter: [...projection.body.query.bool.filter, ...additionalFilters] } }, aggs: { @@ -72,7 +62,7 @@ export async function fetchAndTransformMetrics({ ...aggs } } - }; + }); const response = await client.search(params); diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts new file mode 100644 index 00000000000000..8ee835675e50e7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getCPUChartData } from './by_agent/shared/cpu'; +import { getMemoryChartData } from './by_agent/shared/memory'; +import { getHeapMemoryChart } from './by_agent/java/heap_memory'; +import { getNonHeapMemoryChart } from './by_agent/java/non_heap_memory'; +import { getThreadCountChart } from './by_agent/java/thread_count'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('metrics queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches cpu chart data', async () => { + mock = await inspectSearchParams(setup => getCPUChartData(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches memory chart data', async () => { + mock = await inspectSearchParams(setup => getMemoryChartData(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches heap memory chart data', async () => { + mock = await inspectSearchParams(setup => getHeapMemoryChart(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches non heap memory chart data', async () => { + mock = await inspectSearchParams(setup => + getNonHeapMemoryChart(setup, 'foo') + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches thread count chart data', async () => { + mock = await inspectSearchParams(setup => + getThreadCountChart(setup, 'foo') + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..ddd800dc22bf31 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`services queries fetches the agent status 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "error", + "metric", + "sourcemap", + "transaction", + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + "myIndex", + ], + "terminateAfter": 1, +} +`; + +exports[`services queries fetches the legacy data status 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + ], + }, + }, + Object { + "range": Object { + "observer.version_major": Object { + "lt": 7, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + ], + "terminateAfter": 1, +} +`; + +exports[`services queries fetches the service agent name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "agents": Object { + "terms": Object { + "field": "agent.name", + "size": 1, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "error", + "transaction", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], + "terminate_after": 1, +} +`; + +exports[`services queries fetches the service items 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "aggs": Object { + "agents": Object { + "terms": Object { + "field": "agent.name", + "size": 1, + }, + }, + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "environments": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "events": Object { + "terms": Object { + "field": "processor.event", + "size": 2, + }, + }, + }, + "terms": Object { + "field": "service.name", + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`services queries fetches the service transaction types 1`] = ` +Object { + "body": Object { + "aggs": Object { + "types": Object { + "terms": Object { + "field": "transaction.type", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts index 75410b70e01392..c50506db1faecf 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -5,44 +5,29 @@ */ import { idx } from '@kbn/elastic-idx'; +import { mergeProjection } from '../../../../common/projections/util/merge_projection'; import { PROCESSOR_EVENT, SERVICE_AGENT_NAME, SERVICE_ENVIRONMENT, - SERVICE_NAME, TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../../typings/common'; -import { rangeFilter } from '../../helpers/range_filter'; import { Setup } from '../../helpers/setup_request'; +import { getServicesProjection } from '../../../../common/projections/services'; export type ServiceListAPIResponse = PromiseReturnType; export async function getServicesItems(setup: Setup) { - const { start, end, uiFiltersES, client, config } = setup; + const { start, end, client } = setup; - const params = { - index: [ - config.get('apm_oss.metricsIndices'), - config.get('apm_oss.errorIndices'), - config.get('apm_oss.transactionIndices') - ], + const projection = getServicesProjection({ setup }); + + const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } - }, - { range: rangeFilter(start, end) }, - ...uiFiltersES - ] - } - }, aggs: { services: { terms: { - field: SERVICE_NAME, size: 500 }, aggs: { @@ -62,7 +47,7 @@ export async function getServicesItems(setup: Setup) { } } } - }; + }); const resp = await client.search(params); const aggs = resp.aggregations; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/services/queries.test.ts new file mode 100644 index 00000000000000..83b136112de352 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/queries.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getServiceAgentName } from './get_service_agent_name'; +import { getServiceTransactionTypes } from './get_service_transaction_types'; +import { getServicesItems } from './get_services/get_services_items'; +import { getLegacyDataStatus } from './get_services/get_legacy_data_status'; +import { getAgentStatus } from './get_services/get_agent_status'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('services queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches the service agent name', async () => { + mock = await inspectSearchParams(setup => + getServiceAgentName('foo', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the service transaction types', async () => { + mock = await inspectSearchParams(setup => + getServiceTransactionTypes('foo', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the service items', async () => { + mock = await inspectSearchParams(setup => getServicesItems(setup)); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the legacy data status', async () => { + mock = await inspectSearchParams(setup => getLegacyDataStatus(setup)); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the agent status', async () => { + mock = await inspectSearchParams(setup => getAgentStatus(setup)); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..4c53563aa41dfb --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`agent configuration queries fetches all environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`agent configuration queries fetches configurations 1`] = ` +Object { + "index": "myIndex", +} +`; + +exports[`agent configuration queries fetches filtered configurations with an environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "service.environment": "bar", + }, + }, + ], + }, + }, + "size": 1, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries fetches filtered configurations without an environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "bool": Object { + "must_not": Object { + "exists": Object { + "field": "service.environment", + }, + }, + }, + }, + ], + }, + }, + "size": 1, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries fetches service names 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`agent configuration queries fetches unavailable environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts new file mode 100644 index 00000000000000..768651c8acec73 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.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 { getAllEnvironments } from './get_environments/get_all_environments'; +import { getUnavailableEnvironments } from './get_environments/get_unavailable_environments'; +import { getServiceNames } from './get_service_names'; +import { listConfigurations } from './list_configurations'; +import { searchConfigurations } from './search'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../../public/utils/testHelpers'; + +describe('agent configuration queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches all environments', async () => { + mock = await inspectSearchParams(setup => + getAllEnvironments({ + serviceName: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches unavailable environments', async () => { + mock = await inspectSearchParams(setup => + getUnavailableEnvironments({ + serviceName: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches service names', async () => { + mock = await inspectSearchParams(setup => + getServiceNames({ + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches configurations', async () => { + mock = await inspectSearchParams(setup => + listConfigurations({ + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches filtered configurations without an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + serviceName: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches filtered configurations with an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + serviceName: 'foo', + environment: 'bar', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..396e8540afdd69 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`trace queries fetches a trace 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "trace.id": "foo", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "span", + "transaction", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + "should": Object { + "exists": Object { + "field": "parent.id", + }, + }, + }, + }, + "size": "myIndex", + "sort": Array [ + Object { + "_score": Object { + "order": "asc", + }, + }, + Object { + "transaction.duration.us": Object { + "order": "desc", + }, + }, + Object { + "span.duration.us": Object { + "order": "desc", + }, + }, + ], + }, + "index": Array [ + "myIndex", + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/traces/queries.test.ts new file mode 100644 index 00000000000000..871d0fd1c7fb6b --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/traces/queries.test.ts @@ -0,0 +1,25 @@ +/* + * 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 { getTraceItems } from './get_trace_items'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('trace queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches a trace', async () => { + mock = await inspectSearchParams(setup => getTraceItems('foo', setup)); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index c55e54938aaba6..18ce29982b6167 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -168,7 +168,7 @@ Array [ }, Object { "term": Object { - "service.environment": "test", + "transaction.type": "request", }, }, Object { @@ -178,11 +178,10 @@ Array [ }, Object { "term": Object { - "transaction.type": "request", + "service.environment": "test", }, }, ], - "must_not": Array [], "should": Array [ Object { "term": Object { diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..e33255b5baa554 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -0,0 +1,192 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transaction group queries fetches top traces 1`] = ` +Object { + "body": Object { + "aggs": Object { + "transactions": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "p95": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + ], + }, + }, + "sample": Object { + "top_hits": Object { + "size": 1, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "sum": Object { + "sum": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "transaction.name", + "order": Object { + "sum": "desc", + }, + "size": "myIndex", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + "must_not": Array [ + Object { + "exists": Object { + "field": "parent.id", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction group queries fetches top transactions 1`] = ` +Object { + "body": Object { + "aggs": Object { + "transactions": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "p95": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + ], + }, + }, + "sample": Object { + "top_hits": Object { + "size": 1, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "sum": Object { + "sum": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "transaction.name", + "order": Object { + "sum": "desc", + }, + "size": "myIndex", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts index 3b32776739c81b..a647bd2faff363 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -6,64 +6,48 @@ import { TRANSACTION_DURATION, - TRANSACTION_NAME, - PROCESSOR_EVENT, - PARENT_ID, - TRANSACTION_SAMPLED, - SERVICE_NAME, - TRANSACTION_TYPE + TRANSACTION_SAMPLED } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; import { Setup } from '../helpers/setup_request'; -import { rangeFilter } from '../helpers/range_filter'; -import { BoolQuery } from '../../../typings/elasticsearch'; +import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; interface TopTransactionOptions { type: 'top_transactions'; serviceName: string; transactionType: string; + transactionName?: string; } interface TopTraceOptions { type: 'top_traces'; + transactionName?: string; } export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; export function transactionGroupsFetcher(options: Options, setup: Setup) { - const { client, config, start, end, uiFiltersES } = setup; + const { client, config } = setup; - const bool: BoolQuery = { - must_not: [], - // prefer sampled transactions - should: [{ term: { [TRANSACTION_SAMPLED]: true } }], - filter: [ - { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - ...uiFiltersES - ] - }; + const projection = getTransactionGroupsProjection({ + setup, + options + }); - if (options.type === 'top_traces') { - // A transaction without `parent.id` is considered a "root" transaction, i.e. a trace - bool.must_not.push({ exists: { field: PARENT_ID } }); - } else { - bool.filter.push({ term: { [SERVICE_NAME]: options.serviceName } }); - bool.filter.push({ term: { [TRANSACTION_TYPE]: options.transactionType } }); - } - - const params = { - index: config.get('apm_oss.transactionIndices'), + const params = mergeProjection(projection, { body: { size: 0, query: { - bool + bool: { + // prefer sampled transactions + should: [{ term: { [TRANSACTION_SAMPLED]: true } }] + } }, aggs: { transactions: { terms: { - field: TRANSACTION_NAME, order: { sum: 'desc' }, size: config.get('xpack.apm.ui.transactionGroupBucketSize') }, @@ -86,7 +70,7 @@ export function transactionGroupsFetcher(options: Options, setup: Setup) { } } } - }; + }); return client.search(params); } diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/queries.test.ts new file mode 100644 index 00000000000000..73122d8580134f --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { transactionGroupsFetcher } from './fetcher'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('transaction group queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches top transactions', async () => { + mock = await inspectSearchParams(setup => + transactionGroupsFetcher( + { + type: 'top_transactions', + serviceName: 'foo', + transactionType: 'bar' + }, + setup + ) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches top traces', async () => { + mock = await inspectSearchParams(setup => + transactionGroupsFetcher( + { + type: 'top_traces' + }, + setup + ) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..cdc473b567ea8a --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -0,0 +1,657 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transaction queries fetches a transaction 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.id": "foo", + }, + }, + Object { + "term": Object { + "trace.id": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 1, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches breakdown data for transactions 1`] = ` +Object { + "body": Object { + "aggs": Object { + "by_date": Object { + "aggs": Object { + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches breakdown data for transactions for a transaction name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "by_date": Object { + "aggs": Object { + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "transaction.name": "baz", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction charts 1`] = ` +Object { + "body": Object { + "aggs": Object { + "overall_avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "response_times": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "pct": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + 99, + ], + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "transaction_results": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "missing": "", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` +Object { + "body": Object { + "aggs": Object { + "overall_avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "response_times": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "pct": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + 99, + ], + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "transaction_results": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "missing": "", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "transaction.name": "bar", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "overall_avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "response_times": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "pct": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + 99, + ], + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "transaction_results": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "missing": "", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "transaction.name": "bar", + }, + }, + Object { + "term": Object { + "transaction.type": "baz", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction distribution 1`] = ` +Object { + "body": Object { + "aggs": Object { + "stats": Object { + "extended_stats": Object { + "field": "transaction.duration.us", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "baz", + }, + }, + Object { + "term": Object { + "transaction.name": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/queries.test.ts new file mode 100644 index 00000000000000..13cb1328fdd010 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/queries.test.ts @@ -0,0 +1,107 @@ +/* + * 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 { getTransactionBreakdown } from './breakdown'; +import { getTransactionCharts } from './charts'; +import { getTransactionDistribution } from './distribution'; +import { getTransaction } from './get_transaction'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('transaction queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches breakdown data for transactions', async () => { + mock = await inspectSearchParams(setup => + getTransactionBreakdown({ + serviceName: 'foo', + transactionType: 'bar', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches breakdown data for transactions for a transaction name', async () => { + mock = await inspectSearchParams(setup => + getTransactionBreakdown({ + serviceName: 'foo', + transactionType: 'bar', + transactionName: 'baz', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction charts', async () => { + mock = await inspectSearchParams(setup => + getTransactionCharts({ + serviceName: 'foo', + transactionName: undefined, + transactionType: undefined, + setup + }) + ); + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction charts for a transaction type', async () => { + mock = await inspectSearchParams(setup => + getTransactionCharts({ + serviceName: 'foo', + transactionName: 'bar', + transactionType: undefined, + setup + }) + ); + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction charts for a transaction type and transaction name', async () => { + mock = await inspectSearchParams(setup => + getTransactionCharts({ + serviceName: 'foo', + transactionName: 'bar', + transactionType: 'baz', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction distribution', async () => { + mock = await inspectSearchParams(setup => + getTransactionDistribution({ + serviceName: 'foo', + transactionName: 'bar', + transactionType: 'baz', + traceId: 'qux', + transactionId: 'quz', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches a transaction', async () => { + mock = await inspectSearchParams(setup => + getTransaction('foo', 'bar', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..30e75f46ad5e7c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ui filter queries fetches environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`ui filter queries fetches environments without a service name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap new file mode 100644 index 00000000000000..594b363c1cf7b6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` +Object { + "body": Object { + "aggs": Object { + "host": Object { + "aggs": Object { + "by_terms": Object { + "aggs": Object { + "bucket_count": Object { + "cardinality": Object { + "field": "service.name", + }, + }, + }, + "terms": Object { + "field": "host.hostname", + "order": Object { + "_count": "desc", + }, + }, + }, + }, + "filter": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "transaction.result": Array [ + "2xx", + ], + }, + }, + ], + }, + }, + }, + "transactionResult": Object { + "aggs": Object { + "by_terms": Object { + "aggs": Object { + "bucket_count": Object { + "cardinality": Object { + "field": "service.name", + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "order": Object { + "_count": "desc", + }, + }, + }, + }, + "filter": Object { + "bool": Object { + "filter": Array [], + }, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts new file mode 100644 index 00000000000000..5d10a4ae27060a --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -0,0 +1,77 @@ +/* + * 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 { + CONTAINER_ID, + POD_NAME, + SERVICE_AGENT_NAME, + HOST_NAME, + TRANSACTION_RESULT +} from '../../../../common/elasticsearch_fieldnames'; + +const filtersByName = { + host: { + title: i18n.translate('xpack.apm.localFilters.titles.host', { + defaultMessage: 'Host' + }), + fieldName: HOST_NAME + }, + agentName: { + title: i18n.translate('xpack.apm.localFilters.titles.agentName', { + defaultMessage: 'Agent name' + }), + fieldName: SERVICE_AGENT_NAME + }, + containerId: { + title: i18n.translate('xpack.apm.localFilters.titles.containerId', { + defaultMessage: 'Container ID' + }), + fieldName: CONTAINER_ID + }, + podName: { + title: i18n.translate('xpack.apm.localFilters.titles.podName', { + defaultMessage: 'Pod' + }), + fieldName: POD_NAME + }, + transactionResult: { + title: i18n.translate('xpack.apm.localFilters.titles.transactionResult', { + defaultMessage: 'Transaction result' + }), + fieldName: TRANSACTION_RESULT + } +}; + +export type LocalUIFilterName = keyof typeof filtersByName; + +export interface LocalUIFilter { + name: LocalUIFilterName; + title: string; + fieldName: string; +} + +type LocalUIFilterMap = { + [key in LocalUIFilterName]: LocalUIFilter; +}; + +export const localUIFilterNames = Object.keys( + filtersByName +) as LocalUIFilterName[]; + +export const localUIFilters = localUIFilterNames.reduce( + (acc, key) => { + const field = filtersByName[key]; + + return { + ...acc, + [key]: { + ...field, + name: key + } + }; + }, + {} as LocalUIFilterMap +); diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts new file mode 100644 index 00000000000000..9c9a5c45f697c2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { omit } from 'lodash'; +import { Server } from 'hapi'; +import { Projection } from '../../../../common/projections/typings'; +import { UIFilters } from '../../../../typings/ui-filters'; +import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; +import { localUIFilters, LocalUIFilterName } from './config'; + +export const getFilterAggregations = async ({ + server, + uiFilters, + projection, + localFilterNames +}: { + server: Server; + uiFilters: UIFilters; + projection: Projection; + localFilterNames: LocalUIFilterName[]; +}) => { + const mappedFilters = localFilterNames.map(name => localUIFilters[name]); + + const aggs = await Promise.all( + mappedFilters.map(async field => { + const filter = await getUiFiltersES(server, omit(uiFilters, field.name)); + + const bucketCountAggregation = projection.body.aggs + ? { + aggs: { + bucket_count: { + cardinality: { + field: + projection.body.aggs[Object.keys(projection.body.aggs)[0]] + .terms.field + } + } + } + } + : {}; + + return { + [field.name]: { + filter: { + bool: { + filter + } + }, + aggs: { + by_terms: { + terms: { + field: field.fieldName, + order: { + _count: 'desc' + } + }, + ...bucketCountAggregation + } + } + } + }; + }) + ); + + const mergedAggregations = Object.assign({}, ...aggs) as Partial< + Record + >; + + return mergedAggregations; +}; diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts new file mode 100644 index 00000000000000..abc7f93adbe2a2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -0,0 +1,77 @@ +/* + * 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 { Server } from 'hapi'; +import { cloneDeep, sortByOrder } from 'lodash'; +import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { UIFilters } from '../../../../typings/ui-filters'; +import { Projection } from '../../../../common/projections/typings'; +import { PromiseReturnType } from '../../../../typings/common'; +import { getFilterAggregations } from './get_filter_aggregations'; +import { Setup } from '../../helpers/setup_request'; +import { localUIFilters, LocalUIFilterName } from './config'; + +export type LocalUIFiltersAPIResponse = PromiseReturnType< + typeof getLocalUIFilters +>; + +export async function getLocalUIFilters({ + server, + setup, + projection, + uiFilters, + localFilterNames +}: { + server: Server; + setup: Setup; + projection: Projection; + uiFilters: UIFilters; + localFilterNames: LocalUIFilterName[]; +}) { + const { client } = setup; + + const projectionWithoutAggs = cloneDeep(projection); + + delete projectionWithoutAggs.body.aggs; + + const filterAggregations = await getFilterAggregations({ + server, + uiFilters, + projection, + localFilterNames + }); + + const params = mergeProjection(projectionWithoutAggs, { + body: { + size: 0, + // help TS infer aggregations by making all aggregations required + aggs: filterAggregations as Required + } + }); + + const response = await client.search(params); + + return localFilterNames.map(key => { + const aggregations = response.aggregations[key]; + const filter = localUIFilters[key]; + + return { + ...filter, + options: sortByOrder( + aggregations.by_terms.buckets.map(bucket => { + return { + name: bucket.key, + count: + 'bucket_count' in bucket + ? bucket.bucket_count.value + : bucket.doc_count + }; + }), + 'count', + 'desc' + ) + }; + }); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts new file mode 100644 index 00000000000000..8bcde7c3af7a3f --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getLocalUIFilters } from './'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../../public/utils/testHelpers'; +import { getServicesProjection } from '../../../../common/projections/services'; + +describe('local ui filter queries', () => { + let mock: SearchParamsMock; + + beforeEach(() => { + jest.mock('../../helpers/convert_ui_filters/get_ui_filters_es', () => { + return []; + }); + }); + + afterEach(() => { + mock.teardown(); + }); + + it('fetches local ui filter aggregations', async () => { + mock = await inspectSearchParams(setup => + getLocalUIFilters({ + setup, + localFilterNames: ['transactionResult', 'host'], + projection: getServicesProjection({ setup }), + server: null as any, + uiFilters: { + transactionResult: ['2xx'] + } + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/queries.test.ts new file mode 100644 index 00000000000000..079ab64f32db36 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/queries.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { getEnvironments } from './get_environments'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('ui filter queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches environments', async () => { + mock = await inspectSearchParams(setup => getEnvironments(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches environments without a service name', async () => { + mock = await inspectSearchParams(setup => getEnvironments(setup)); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts index 9f6903c9840a68..28a44bd3e9e376 100644 --- a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts @@ -5,11 +5,24 @@ */ import Boom from 'boom'; -import Joi from 'joi'; +import Joi, { Schema } from 'joi'; import { InternalCoreSetup } from 'src/core/server'; +import { omit } from 'lodash'; import { withDefaultValidators } from '../lib/helpers/input_validation'; -import { setupRequest } from '../lib/helpers/setup_request'; +import { setupRequest, Setup } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; +import { PROJECTION, Projection } from '../../common/projections/typings'; +import { + LocalUIFilterName, + localUIFilterNames +} from '../lib/ui_filters/local_ui_filters/config'; +import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; +import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; +import { getServicesProjection } from '../../common/projections/services'; +import { getTransactionGroupsProjection } from '../../common/projections/transaction_groups'; +import { getMetricsProjection } from '../../common/projections/metrics'; +import { getErrorGroupsProjection } from '../../common/projections/errors'; +import { getTransactionsProjection } from '../../common/projections/transactions'; const defaultErrorHandler = (err: Error) => { // eslint-disable-next-line @@ -38,4 +51,161 @@ export function initUIFiltersApi(core: InternalCoreSetup) { return getEnvironments(setup, serviceName).catch(defaultErrorHandler); } }); + + const createLocalFiltersEndpoint = ({ + name, + getProjection, + validators + }: { + name: PROJECTION; + getProjection: ({ + setup, + query + }: { + setup: Setup; + query: Record; + }) => Projection; + validators?: Record; + }) => { + server.route({ + method: 'GET', + path: `/api/apm/ui_filters/local_filters/${name}`, + options: { + validate: { + query: withDefaultValidators({ + filterNames: Joi.array() + .items(localUIFilterNames) + .required(), + ...validators + }) + }, + tags: ['access:apm'] + }, + handler: async req => { + const setup = await setupRequest(req); + + const { uiFilters, filterNames } = (req.query as unknown) as { + uiFilters: string; + filterNames: LocalUIFilterName[]; + }; + + const parsedUiFilters = JSON.parse(uiFilters); + + const projection = getProjection({ + query: req.query as Record, + setup: { + ...setup, + uiFiltersES: await getUiFiltersES( + req.server, + omit(parsedUiFilters, filterNames) + ) + } + }); + + return getLocalUIFilters({ + server: req.server, + projection, + setup, + uiFilters: parsedUiFilters, + localFilterNames: filterNames + }).catch(defaultErrorHandler); + } + }); + }; + + createLocalFiltersEndpoint({ + name: PROJECTION.SERVICES, + getProjection: ({ setup }) => { + return getServicesProjection({ setup }); + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.TRACES, + getProjection: ({ setup }) => { + return getTransactionGroupsProjection({ + setup, + options: { type: 'top_traces' } + }); + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.TRANSACTION_GROUPS, + getProjection: ({ setup, query }) => { + const { transactionType, serviceName, transactionName } = query as { + transactionType: string; + serviceName: string; + transactionName?: string; + }; + return getTransactionGroupsProjection({ + setup, + options: { + type: 'top_transactions', + transactionType, + serviceName, + transactionName + } + }); + }, + validators: { + serviceName: Joi.string().required(), + transactionType: Joi.string().required(), + transactionName: Joi.string() + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.TRANSACTIONS, + getProjection: ({ setup, query }) => { + const { transactionType, serviceName, transactionName } = query as { + transactionType: string; + serviceName: string; + transactionName: string; + }; + return getTransactionsProjection({ + setup, + transactionType, + serviceName, + transactionName + }); + }, + validators: { + serviceName: Joi.string().required(), + transactionType: Joi.string().required(), + transactionName: Joi.string().required() + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.METRICS, + getProjection: ({ setup, query }) => { + const { serviceName } = query as { + serviceName: string; + }; + return getMetricsProjection({ + setup, + serviceName + }); + }, + validators: { + serviceName: Joi.string().required() + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.ERROR_GROUPS, + getProjection: ({ setup, query }) => { + const { serviceName } = query as { + serviceName: string; + }; + return getErrorGroupsProjection({ + setup, + serviceName + }); + }, + validators: { + serviceName: Joi.string().required() + } + }); } diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index f424ea21ab34b5..150ec70286acca 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -25,7 +25,10 @@ declare module 'elasticsearch' { | 'min' | 'percentiles' | 'sum' - | 'extended_stats'; + | 'extended_stats' + | 'filter' + | 'filters' + | 'cardinality'; type AggOptions = AggregationOptionMap & { [key: string]: any; @@ -40,6 +43,10 @@ declare module 'elasticsearch' { }; }; + type SubAggregation = T extends { aggs: any } + ? AggregationResultMap + : {}; + // eslint-disable-next-line @typescript-eslint/prefer-interface type BucketAggregation = { buckets: Array< @@ -47,9 +54,20 @@ declare module 'elasticsearch' { key: KeyType; key_as_string: string; doc_count: number; - } & (SubAggregationMap extends { aggs: any } - ? AggregationResultMap - : {}) + } & (SubAggregation) + >; + }; + + type FilterAggregation = { + doc_count: number; + } & SubAggregation; + + // eslint-disable-next-line @typescript-eslint/prefer-interface + type FiltersAggregation = { + buckets: Array< + { + doc_count: number; + } & SubAggregation >; }; @@ -105,6 +123,11 @@ declare module 'elasticsearch' { lower: number | null; }; }; + filter: FilterAggregation; + filters: FiltersAggregation; + cardinality: { + value: number; + }; }[AggregationType & keyof AggregationOption[AggregationName]]; } >; diff --git a/x-pack/legacy/plugins/apm/typings/ui-filters.ts b/x-pack/legacy/plugins/apm/typings/ui-filters.ts index 6566f5c9471a2b..0c62ef3b1c962b 100644 --- a/x-pack/legacy/plugins/apm/typings/ui-filters.ts +++ b/x-pack/legacy/plugins/apm/typings/ui-filters.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface UIFilters { +import { LocalUIFilterName } from '../server/lib/ui_filters/local_ui_filters/config'; + +export type UIFilters = { kuery?: string; environment?: string; -} +} & { [key in LocalUIFilterName]?: string[] }; diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.js b/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.ts similarity index 95% rename from x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.js rename to x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.ts index 9bae58f385f5e7..899fbf3d3d91bc 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.js +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore Untyped Library import { Fn } from '@kbn/interpreter/common'; import { functions as browserFns } from '../../canvas_plugin_src/functions/browser'; import { functions as commonFns } from '../../canvas_plugin_src/functions/common'; diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.js b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.js deleted file mode 100644 index f2db3326be189b..00000000000000 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const workpads = [ - { - pages: [ - { - elements: [ - { - expression: ` - demodata | - ply by=age fn={rowCount | as count} | - staticColumn total value={math 'sum(count)'} | - mapColumn percentage fn={math 'count/total * 100'} | - sort age | - pointseries x=age y=percentage | - plot defaultStyle={seriesStyle points=0 lines=5}`, - }, - ], - }, - ], - }, - { - pages: [{ elements: [{ expression: 'filters | demodata | markdown "hello" | render' }] }], - }, - { - pages: [ - { - elements: [ - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'filters | demodata | markdown "hello" | render' }, - { expression: 'filters | demodata | pointseries | pie | render' }, - ], - }, - { elements: [{ expression: 'filters | demodata | table | render' }] }, - { elements: [{ expression: 'image | render' }] }, - { elements: [{ expression: 'image | render' }] }, - ], - }, - { - pages: [ - { - elements: [ - { expression: 'filters | demodata | markdown "hello" | render' }, - { expression: 'filters | demodata | markdown "hello" | render' }, - { expression: 'image | render' }, - ], - }, - { - elements: [ - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'filters | demodata | markdown "hello" | render' }, - { expression: 'filters | demodata | pointseries | pie | render' }, - { expression: 'image | render' }, - ], - }, - { - elements: [ - { expression: 'filters | demodata | pointseries | pie | render' }, - { - expression: - 'filters | demodata | pointseries | plot defaultStyle={seriesStyle points=0 lines=5} | render', - }, - ], - }, - ], - }, - { - pages: [ - { - elements: [ - { expression: 'demodata | render as=debug' }, - { expression: 'filters | demodata | pointseries | plot | render' }, - { expression: 'filters | demodata | table | render' }, - { expression: 'filters | demodata | table | render' }, - ], - }, - { - elements: [ - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'filters | demodata | pointseries | pie | render' }, - { expression: 'image | render' }, - ], - }, - { - elements: [ - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'demodata | render as=debug' }, - { expression: 'shape "square" | render' }, - ], - }, - ], - }, - { - pages: [ - { - elements: [ - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'filters | demodata | markdown "hello" | render' }, - ], - }, - { elements: [{ expression: 'image | render' }] }, - { elements: [{ expression: 'image | render' }] }, - { elements: [{ expression: 'filters | demodata | table | render' }] }, - ], - }, -]; - -export const elements = [ - { expression: 'demodata | pointseries | getCell | repeatImage | render' }, - { expression: 'filters | demodata | markdown "hello" | render' }, - { expression: 'filters | demodata | pointseries | pie | render' }, - { expression: 'image | render' }, -]; diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts new file mode 100644 index 00000000000000..0251095c9e75e3 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts @@ -0,0 +1,184 @@ +/* + * 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 { CanvasWorkpad, CanvasElement, CanvasPage } from '../../types'; + +const BaseWorkpad: CanvasWorkpad = { + name: 'base workpad', + id: 'base-workpad', + width: 0, + height: 0, + css: '', + page: 1, + pages: [], + colors: [], + isWriteable: true, +}; + +const BasePage: CanvasPage = { + id: 'base-page', + style: { background: 'white' }, + transition: {}, + elements: [], + groups: [], +}; +const BaseElement: CanvasElement = { + position: { + top: 0, + left: 0, + width: 0, + height: 0, + angle: 0, + parent: null, + }, + id: 'base-id', + type: 'element', + expression: 'render', + filter: '', +}; + +export const workpads: CanvasWorkpad[] = [ + { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { + ...BaseElement, + expression: ` + demodata | + ply by=age fn={rowCount | as count} | + staticColumn total value={math 'sum(count)'} | + mapColumn percentage fn={math 'count/total * 100'} | + sort age | + pointseries x=age y=percentage | + plot defaultStyle={seriesStyle points=0 lines=5}`, + }, + ], + }, + ], + }, + { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' }, + ], + }, + ], + }, + { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' }, + { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, + ], + }, + { + ...BasePage, + elements: [{ ...BaseElement, expression: 'filters | demodata | table | render' }], + }, + { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, + { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, + ], + }, + { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' }, + { ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' }, + { ...BaseElement, expression: 'image | render' }, + ], + }, + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' }, + { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, + { ...BaseElement, expression: 'image | render' }, + ], + }, + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, + { + ...BaseElement, + expression: + 'filters | demodata | pointseries | plot defaultStyle={seriesStyle points=0 lines=5} | render', + }, + ], + }, + ], + }, + { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'demodata | render as=debug' }, + { ...BaseElement, expression: 'filters | demodata | pointseries | plot | render' }, + { ...BaseElement, expression: 'filters | demodata | table | render' }, + { ...BaseElement, expression: 'filters | demodata | table | render' }, + ], + }, + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, + { ...BaseElement, expression: 'image | render' }, + ], + }, + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'demodata | render as=debug' }, + { ...BaseElement, expression: 'shape "square" | render' }, + ], + }, + ], + }, + { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' }, + ], + }, + { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, + { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, + { + ...BasePage, + elements: [{ ...BaseElement, expression: 'filters | demodata | table | render' }], + }, + ], + }, +]; + +export const elements: CanvasElement[] = [ + { ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' }, + { ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' }, + { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, + { ...BaseElement, expression: 'image | render' }, +]; diff --git a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.js b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.js deleted file mode 100644 index 4b6417aed8e98f..00000000000000 --- a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.js +++ /dev/null @@ -1,231 +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 { uniq } from 'lodash'; -import { parse, getByAlias } from '@kbn/interpreter/common'; - -const MARKER = 'CANVAS_SUGGESTION_MARKER'; - -/** - * Generates the AST with the given expression and then returns the function and argument definitions - * at the given position in the expression, if there are any. - */ -export function getFnArgDefAtPosition(specs, expression, position) { - const text = expression.substr(0, position) + MARKER + expression.substr(position); - try { - const ast = parse(text, { addMeta: true }); - const { ast: newAst, fnIndex, argName } = getFnArgAtPosition(ast, position); - const fn = newAst.node.chain[fnIndex].node; - - const fnDef = getByAlias(specs, fn.function.replace(MARKER, '')); - if (fnDef && argName) { - const argDef = getByAlias(fnDef.args, argName); - return { fnDef, argDef }; - } - return { fnDef }; - } catch (e) { - // Fail silently - } - return []; -} - -/** - * Gets a list of suggestions for the given expression at the given position. It does this by - * inserting a marker at the given position, then parsing the resulting expression. This way we can - * see what the marker would turn into, which tells us what sorts of things to suggest. For - * example, if the marker turns into a function name, then we suggest functions. If it turns into - * an unnamed argument, we suggest argument names. If it turns into a value, we suggest values. - */ -export function getAutocompleteSuggestions(specs, expression, position) { - const text = expression.substr(0, position) + MARKER + expression.substr(position); - try { - const ast = parse(text, { addMeta: true }); - const { ast: newAst, fnIndex, argName, argIndex } = getFnArgAtPosition(ast, position); - const fn = newAst.node.chain[fnIndex].node; - - if (fn.function.includes(MARKER)) { - return getFnNameSuggestions(specs, newAst, fnIndex); - } - - if (argName === '_') { - return getArgNameSuggestions(specs, newAst, fnIndex, argName, argIndex); - } - - if (argName) { - return getArgValueSuggestions(specs, newAst, fnIndex, argName, argIndex); - } - } catch (e) { - // Fail silently - } - return []; -} - -/** - * Get the function and argument (if there is one) at the given position. - */ -function getFnArgAtPosition(ast, position) { - const fnIndex = ast.node.chain.findIndex(fn => fn.start <= position && position <= fn.end); - const fn = ast.node.chain[fnIndex]; - for (const [argName, argValues] of Object.entries(fn.node.arguments)) { - for (let argIndex = 0; argIndex < argValues.length; argIndex++) { - const value = argValues[argIndex]; - if (value.start <= position && position <= value.end) { - if (value.node !== null && value.node.type === 'expression') { - return getFnArgAtPosition(value, position); - } - return { ast, fnIndex, argName, argIndex }; - } - } - } - return { ast, fnIndex }; -} - -function getFnNameSuggestions(specs, ast, fnIndex) { - // Filter the list of functions by the text at the marker - const { start, end, node: fn } = ast.node.chain[fnIndex]; - const query = fn.function.replace(MARKER, ''); - const matchingFnDefs = specs.filter(({ name }) => textMatches(name, query)); - - // Sort by whether or not the function expects the previous function's return type, then by - // whether or not the function name starts with the text at the marker, then alphabetically - const prevFn = ast.node.chain[fnIndex - 1]; - const prevFnDef = prevFn && getByAlias(specs, prevFn.node.function); - const prevFnType = prevFnDef && prevFnDef.type; - const comparator = combinedComparator( - prevFnTypeComparator(prevFnType), - invokeWithProp(startsWithComparator(query), 'name'), - invokeWithProp(alphanumericalComparator, 'name') - ); - const fnDefs = matchingFnDefs.sort(comparator); - - return fnDefs.map(fnDef => { - return { type: 'function', text: fnDef.name + ' ', start, end: end - MARKER.length, fnDef }; - }); -} - -function getArgNameSuggestions(specs, ast, fnIndex, argName, argIndex) { - // Get the list of args from the function definition - const fn = ast.node.chain[fnIndex].node; - const fnDef = getByAlias(specs, fn.function); - if (!fnDef) { - return []; - } - - // We use the exact text instead of the value because it is always a string and might be quoted - const { text, start, end } = fn.arguments[argName][argIndex]; - - // Filter the list of args by the text at the marker - const query = text.replace(MARKER, ''); - const matchingArgDefs = Object.values(fnDef.args).filter(({ name }) => textMatches(name, query)); - - // Filter the list of args by those which aren't already present (unless they allow multi) - const argEntries = Object.entries(fn.arguments).map(([name, values]) => { - return [name, values.filter(value => !value.text.includes(MARKER))]; - }); - const unusedArgDefs = matchingArgDefs.filter(argDef => { - if (argDef.multi) { - return true; - } - return !argEntries.some(([name, values]) => { - return values.length && (name === argDef.name || argDef.aliases.includes(name)); - }); - }); - - // Sort by whether or not the arg is also the unnamed, then by whether or not the arg name starts - // with the text at the marker, then alphabetically - const comparator = combinedComparator( - unnamedArgComparator, - invokeWithProp(startsWithComparator(query), 'name'), - invokeWithProp(alphanumericalComparator, 'name') - ); - const argDefs = unusedArgDefs.sort(comparator); - - return argDefs.map(argDef => { - return { type: 'argument', text: argDef.name + '=', start, end: end - MARKER.length, argDef }; - }); -} - -function getArgValueSuggestions(specs, ast, fnIndex, argName, argIndex) { - // Get the list of values from the argument definition - const fn = ast.node.chain[fnIndex].node; - const fnDef = getByAlias(specs, fn.function); - if (!fnDef) { - return []; - } - const argDef = getByAlias(fnDef.args, argName); - if (!argDef) { - return []; - } - - // Get suggestions from the argument definition, including the default - const { start, end, node } = fn.arguments[argName][argIndex]; - const query = node.replace(MARKER, ''); - const suggestions = uniq(argDef.options.concat(argDef.default || [])); - - // Filter the list of suggestions by the text at the marker - const filtered = suggestions.filter(option => textMatches(String(option), query)); - - // Sort by whether or not the value starts with the text at the marker, then alphabetically - const comparator = combinedComparator(startsWithComparator(query), alphanumericalComparator); - const sorted = filtered.sort(comparator); - - return sorted.map(value => { - const text = maybeQuote(value) + ' '; - return { start, end: end - MARKER.length, type: 'value', text }; - }); -} - -function textMatches(text, query) { - return text.toLowerCase().includes(query.toLowerCase().trim()); -} - -function maybeQuote(value) { - if (typeof value === 'string') { - if (value.match(/^\{.*\}$/)) { - return value; - } - return `"${value.replace(/"/g, '\\"')}"`; - } - return value; -} - -function prevFnTypeComparator(prevFnType) { - return (a, b) => - Boolean(b.context.types && b.context.types.includes(prevFnType)) - - Boolean(a.context.types && a.context.types.includes(prevFnType)); -} - -function unnamedArgComparator(a, b) { - return b.aliases.includes('_') - a.aliases.includes('_'); -} - -function alphanumericalComparator(a, b) { - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - return 0; -} - -function startsWithComparator(query) { - return (a, b) => String(b).startsWith(query) - String(a).startsWith(query); -} - -function combinedComparator(...comparators) { - return (a, b) => - comparators.reduce((acc, comparator) => { - if (acc !== 0) { - return acc; - } - return comparator(a, b); - }, 0); -} - -function invokeWithProp(fn, prop) { - return (...args) => fn(...args.map(arg => arg[prop])); -} diff --git a/x-pack/legacy/plugins/canvas/common/lib/__tests__/autocomplete.js b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts similarity index 69% rename from x-pack/legacy/plugins/canvas/common/lib/__tests__/autocomplete.js rename to x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts index d4008c7f9b48ef..8ee9991ec7db45 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/__tests__/autocomplete.js +++ b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts @@ -4,34 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { functionSpecs } from '../../../__tests__/fixtures/function_specs'; -import { getAutocompleteSuggestions } from '../autocomplete'; +import { functionSpecs } from '../../__tests__/fixtures/function_specs'; + +import { getAutocompleteSuggestions } from './autocomplete'; describe('getAutocompleteSuggestions', () => { it('should suggest functions', () => { const suggestions = getAutocompleteSuggestions(functionSpecs, '', 0); - expect(suggestions.length).to.be(functionSpecs.length); - expect(suggestions[0].start).to.be(0); - expect(suggestions[0].end).to.be(0); + expect(suggestions.length).toBe(functionSpecs.length); + expect(suggestions[0].start).toBe(0); + expect(suggestions[0].end).toBe(0); }); it('should suggest functions filtered by text', () => { const expression = 'pl'; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, 0); const nonmatching = suggestions.map(s => s.text).filter(text => !text.includes(expression)); - expect(nonmatching.length).to.be(0); - expect(suggestions[0].start).to.be(0); - expect(suggestions[0].end).to.be(expression.length); + expect(nonmatching.length).toBe(0); + expect(suggestions[0].start).toBe(0); + expect(suggestions[0].end).toBe(expression.length); }); it('should suggest arguments', () => { const expression = 'plot '; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const plotFn = functionSpecs.find(spec => spec.name === 'plot'); - expect(suggestions.length).to.be(Object.keys(plotFn.args).length); - expect(suggestions[0].start).to.be(expression.length); - expect(suggestions[0].end).to.be(expression.length); + expect(suggestions.length).toBe(Object.keys(plotFn.args).length); + expect(suggestions[0].start).toBe(expression.length); + expect(suggestions[0].end).toBe(expression.length); }); it('should suggest arguments filtered by text', () => { @@ -39,28 +39,28 @@ describe('getAutocompleteSuggestions', () => { const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const plotFn = functionSpecs.find(spec => spec.name === 'plot'); const matchingArgs = Object.keys(plotFn.args).filter(key => key.includes('axis')); - expect(suggestions.length).to.be(matchingArgs.length); - expect(suggestions[0].start).to.be('plot '.length); - expect(suggestions[0].end).to.be('plot axis'.length); + expect(suggestions.length).toBe(matchingArgs.length); + expect(suggestions[0].start).toBe('plot '.length); + expect(suggestions[0].end).toBe('plot axis'.length); }); it('should suggest values', () => { const expression = 'shape shape='; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const shapeFn = functionSpecs.find(spec => spec.name === 'shape'); - expect(suggestions.length).to.be(shapeFn.args.shape.options.length); - expect(suggestions[0].start).to.be(expression.length); - expect(suggestions[0].end).to.be(expression.length); + expect(suggestions.length).toBe(shapeFn.args.shape.options.length); + expect(suggestions[0].start).toBe(expression.length); + expect(suggestions[0].end).toBe(expression.length); }); it('should suggest values filtered by text', () => { const expression = 'shape shape=ar'; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const shapeFn = functionSpecs.find(spec => spec.name === 'shape'); - const matchingValues = shapeFn.args.shape.options.filter(key => key.includes('ar')); - expect(suggestions.length).to.be(matchingValues.length); - expect(suggestions[0].start).to.be(expression.length - 'ar'.length); - expect(suggestions[0].end).to.be(expression.length); + const matchingValues = shapeFn.args.shape.options.filter((key: string) => key.includes('ar')); + expect(suggestions.length).toBe(matchingValues.length); + expect(suggestions[0].start).toBe(expression.length - 'ar'.length); + expect(suggestions[0].end).toBe(expression.length); }); it('should suggest functions inside an expression', () => { @@ -70,9 +70,9 @@ describe('getAutocompleteSuggestions', () => { expression, expression.length - 1 ); - expect(suggestions.length).to.be(functionSpecs.length); - expect(suggestions[0].start).to.be(expression.length - 1); - expect(suggestions[0].end).to.be(expression.length - 1); + expect(suggestions.length).toBe(functionSpecs.length); + expect(suggestions[0].start).toBe(expression.length - 1); + expect(suggestions[0].end).toBe(expression.length - 1); }); it('should suggest arguments inside an expression', () => { @@ -83,9 +83,9 @@ describe('getAutocompleteSuggestions', () => { expression.length - 1 ); const ltFn = functionSpecs.find(spec => spec.name === 'lt'); - expect(suggestions.length).to.be(Object.keys(ltFn.args).length); - expect(suggestions[0].start).to.be(expression.length - 1); - expect(suggestions[0].end).to.be(expression.length - 1); + expect(suggestions.length).toBe(Object.keys(ltFn.args).length); + expect(suggestions[0].start).toBe(expression.length - 1); + expect(suggestions[0].end).toBe(expression.length - 1); }); it('should suggest values inside an expression', () => { @@ -96,9 +96,9 @@ describe('getAutocompleteSuggestions', () => { expression.length - 1 ); const shapeFn = functionSpecs.find(spec => spec.name === 'shape'); - expect(suggestions.length).to.be(shapeFn.args.shape.options.length); - expect(suggestions[0].start).to.be(expression.length - 1); - expect(suggestions[0].end).to.be(expression.length - 1); + expect(suggestions.length).toBe(shapeFn.args.shape.options.length); + expect(suggestions[0].start).toBe(expression.length - 1); + expect(suggestions[0].end).toBe(expression.length - 1); }); it('should suggest values inside quotes', () => { @@ -109,10 +109,10 @@ describe('getAutocompleteSuggestions', () => { expression.length - 1 ); const shapeFn = functionSpecs.find(spec => spec.name === 'shape'); - const matchingValues = shapeFn.args.shape.options.filter(key => key.includes('ar')); - expect(suggestions.length).to.be(matchingValues.length); - expect(suggestions[0].start).to.be(expression.length - '"ar"'.length); - expect(suggestions[0].end).to.be(expression.length); + const matchingValues = shapeFn.args.shape.options.filter((key: string) => key.includes('ar')); + expect(suggestions.length).toBe(matchingValues.length); + expect(suggestions[0].start).toBe(expression.length - '"ar"'.length); + expect(suggestions[0].end).toBe(expression.length); }); it('should prioritize functions that start with text', () => { @@ -122,7 +122,7 @@ describe('getAutocompleteSuggestions', () => { const alterColumnIndex = suggestions.findIndex(suggestion => suggestion.text.includes('alterColumn') ); - expect(tableIndex).to.be.lessThan(alterColumnIndex); + expect(tableIndex).toBeLessThan(alterColumnIndex); }); it('should prioritize functions that match the previous function type', () => { @@ -130,7 +130,7 @@ describe('getAutocompleteSuggestions', () => { const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const renderIndex = suggestions.findIndex(suggestion => suggestion.text.includes('render')); const anyIndex = suggestions.findIndex(suggestion => suggestion.text.includes('any')); - expect(renderIndex).to.be.lessThan(anyIndex); + expect(renderIndex).toBeLessThan(anyIndex); }); it('should alphabetize functions', () => { @@ -138,7 +138,7 @@ describe('getAutocompleteSuggestions', () => { const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const metricIndex = suggestions.findIndex(suggestion => suggestion.text.includes('metric')); const anyIndex = suggestions.findIndex(suggestion => suggestion.text.includes('any')); - expect(anyIndex).to.be.lessThan(metricIndex); + expect(anyIndex).toBeLessThan(metricIndex); }); it('should prioritize arguments that start with text', () => { @@ -148,7 +148,7 @@ describe('getAutocompleteSuggestions', () => { const defaultStyleIndex = suggestions.findIndex(suggestion => suggestion.text.includes('defaultStyle') ); - expect(yaxisIndex).to.be.lessThan(defaultStyleIndex); + expect(yaxisIndex).toBeLessThan(defaultStyleIndex); }); it('should prioritize unnamed arguments', () => { @@ -156,7 +156,7 @@ describe('getAutocompleteSuggestions', () => { const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const whenIndex = suggestions.findIndex(suggestion => suggestion.text.includes('when')); const thenIndex = suggestions.findIndex(suggestion => suggestion.text.includes('then')); - expect(whenIndex).to.be.lessThan(thenIndex); + expect(whenIndex).toBeLessThan(thenIndex); }); it('should alphabetize arguments', () => { @@ -166,24 +166,24 @@ describe('getAutocompleteSuggestions', () => { const defaultStyleIndex = suggestions.findIndex(suggestion => suggestion.text.includes('defaultStyle') ); - expect(defaultStyleIndex).to.be.lessThan(yaxisIndex); + expect(defaultStyleIndex).toBeLessThan(yaxisIndex); }); it('should quote string values', () => { const expression = 'shape shape='; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); - expect(suggestions[0].text.trim()).to.match(/^".*"$/); + expect(suggestions[0].text.trim()).toMatch(/^".*"$/); }); it('should not quote sub expression value suggestions', () => { const expression = 'plot font='; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); - expect(suggestions[0].text.trim()).to.be('{font}'); + expect(suggestions[0].text.trim()).toBe('{font}'); }); it('should not quote booleans', () => { const expression = 'table paginate=true'; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); - expect(suggestions[0].text.trim()).to.be('true'); + expect(suggestions[0].text.trim()).toBe('true'); }); }); diff --git a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts new file mode 100644 index 00000000000000..c8f7e13bfb3134 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts @@ -0,0 +1,376 @@ +/* + * 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 { uniq } from 'lodash'; +// @ts-ignore Untyped Library +import { parse, getByAlias as untypedGetByAlias } from '@kbn/interpreter/common'; +import { + ExpressionAST, + ExpressionFunctionAST, + ExpressionArgAST, + CanvasFunction, +} from '../../types'; + +const MARKER = 'CANVAS_SUGGESTION_MARKER'; + +// If you parse an expression with the "addMeta" option it completely +// changes the type of returned object. The following types +// enhance the existing AST types with the appropriate meta information +interface ASTMetaInformation { + start: number; + end: number; + text: string; + node: T; +} + +// Wraps ExpressionArg with meta or replace ExpressionAST with ExpressionASTWithMeta +type WrapExpressionArgWithMeta = T extends ExpressionAST + ? ExpressionASTWithMeta + : ASTMetaInformation; + +type ExpressionArgASTWithMeta = WrapExpressionArgWithMeta; + +type Modify = Pick> & R; + +// Wrap ExpressionFunctionAST with meta and modify arguments to be wrapped with meta +type ExpressionFunctionASTWithMeta = Modify< + ExpressionFunctionAST, + { + arguments: { + [key: string]: ExpressionArgASTWithMeta[]; + }; + } +>; + +// Wrap ExpressionFunctionAST with meta and modify chain to be wrapped with meta +type ExpressionASTWithMeta = ASTMetaInformation< + Modify< + ExpressionAST, + { + chain: Array>; + } + > +>; + +// Typeguard for checking if ExpressionArg is a new expression +function isExpression( + maybeExpression: ExpressionArgASTWithMeta +): maybeExpression is ExpressionASTWithMeta { + return typeof maybeExpression.node === 'object'; +} + +type valueof = T[keyof T]; +type ValuesOfUnion = T extends any ? valueof : never; + +// All of the possible Arg Values +type ArgValue = ValuesOfUnion; +// All of the argument objects +type CanvasArg = CanvasFunction['args']; + +// Overloads to change return type based on specs +function getByAlias(specs: CanvasFunction[], name: string): CanvasFunction; +// eslint-disable-next-line @typescript-eslint/unified-signatures +function getByAlias(specs: CanvasArg, name: string): ArgValue; +function getByAlias(specs: CanvasFunction[] | CanvasArg, name: string): CanvasFunction | ArgValue { + return untypedGetByAlias(specs, name); +} + +/** + * Generates the AST with the given expression and then returns the function and argument definitions + * at the given position in the expression, if there are any. + */ +export function getFnArgDefAtPosition( + specs: CanvasFunction[], + expression: string, + position: number +) { + const text = expression.substr(0, position) + MARKER + expression.substr(position); + try { + const ast: ExpressionASTWithMeta = parse(text, { addMeta: true }) as ExpressionASTWithMeta; + + const { ast: newAst, fnIndex, argName } = getFnArgAtPosition(ast, position); + const fn = newAst.node.chain[fnIndex].node; + + const fnDef = getByAlias(specs, fn.function.replace(MARKER, '')); + if (fnDef && argName) { + const argDef = getByAlias(fnDef.args, argName); + return { fnDef, argDef }; + } + return { fnDef }; + } catch (e) { + // Fail silently + } + return []; +} + +/** + * Gets a list of suggestions for the given expression at the given position. It does this by + * inserting a marker at the given position, then parsing the resulting expression. This way we can + * see what the marker would turn into, which tells us what sorts of things to suggest. For + * example, if the marker turns into a function name, then we suggest functions. If it turns into + * an unnamed argument, we suggest argument names. If it turns into a value, we suggest values. + */ +export function getAutocompleteSuggestions( + specs: CanvasFunction[], + expression: string, + position: number +) { + const text = expression.substr(0, position) + MARKER + expression.substr(position); + try { + const ast = parse(text, { addMeta: true }) as ExpressionASTWithMeta; + const { ast: newAst, fnIndex, argName, argIndex } = getFnArgAtPosition(ast, position); + const fn = newAst.node.chain[fnIndex].node; + + if (fn.function.includes(MARKER)) { + return getFnNameSuggestions(specs, newAst, fnIndex); + } + + if (argName === '_' && argIndex !== undefined) { + return getArgNameSuggestions(specs, newAst, fnIndex, argName, argIndex); + } + + if (argName && argIndex !== undefined) { + return getArgValueSuggestions(specs, newAst, fnIndex, argName, argIndex); + } + } catch (e) { + // Fail silently + } + return []; +} + +/** + Each entry of the node.chain has it's overall start and end position. For instance, + given the expression "link arg='something' | render" the link functions start position is 0 and end + position is 21. + + This function is given the full ast and the current cursor position in the expression string. + + It returns which function the cursor is in, as well as which argument for that function the cursor is in + if any. +*/ +function getFnArgAtPosition( + ast: ExpressionASTWithMeta, + position: number +): { ast: ExpressionASTWithMeta; fnIndex: number; argName?: string; argIndex?: number } { + const fnIndex = ast.node.chain.findIndex(fn => fn.start <= position && position <= fn.end); + const fn = ast.node.chain[fnIndex]; + for (const [argName, argValues] of Object.entries(fn.node.arguments)) { + for (let argIndex = 0; argIndex < argValues.length; argIndex++) { + const value = argValues[argIndex]; + if (value.start <= position && position <= value.end) { + if (value.node !== null && isExpression(value)) { + return getFnArgAtPosition(value, position); + } + return { ast, fnIndex, argName, argIndex }; + } + } + } + return { ast, fnIndex }; +} + +function getFnNameSuggestions( + specs: CanvasFunction[], + ast: ExpressionASTWithMeta, + fnIndex: number +) { + // Filter the list of functions by the text at the marker + const { start, end, node: fn } = ast.node.chain[fnIndex]; + const query = fn.function.replace(MARKER, ''); + const matchingFnDefs = specs.filter(({ name }) => textMatches(name, query)); + + // Sort by whether or not the function expects the previous function's return type, then by + // whether or not the function name starts with the text at the marker, then alphabetically + const prevFn = ast.node.chain[fnIndex - 1]; + + const prevFnDef = prevFn && getByAlias(specs, prevFn.node.function); + const prevFnType = prevFnDef && prevFnDef.type; + const comparator = combinedComparator( + prevFnTypeComparator(prevFnType), + invokeWithProp(startsWithComparator(query), 'name'), + invokeWithProp(alphanumericalComparator, 'name') + ); + const fnDefs = matchingFnDefs.sort(comparator); + + return fnDefs.map(fnDef => { + return { type: 'function', text: fnDef.name + ' ', start, end: end - MARKER.length, fnDef }; + }); +} + +function getArgNameSuggestions( + specs: CanvasFunction[], + ast: ExpressionASTWithMeta, + fnIndex: number, + argName: string, + argIndex: number +) { + // Get the list of args from the function definition + const fn = ast.node.chain[fnIndex].node; + const fnDef = getByAlias(specs, fn.function); + if (!fnDef) { + return []; + } + + // We use the exact text instead of the value because it is always a string and might be quoted + const { text, start, end } = fn.arguments[argName][argIndex]; + + // Filter the list of args by the text at the marker + const query = text.replace(MARKER, ''); + const matchingArgDefs = Object.entries(fnDef.args).filter(([name]) => + textMatches(name, query) + ); + + // Filter the list of args by those which aren't already present (unless they allow multi) + const argEntries = Object.entries(fn.arguments).map<[string, ExpressionArgASTWithMeta[]]>( + ([name, values]) => { + return [name, values.filter(value => !value.text.includes(MARKER))]; + } + ); + + const unusedArgDefs = matchingArgDefs.filter(([matchingArgName, matchingArgDef]) => { + if (matchingArgDef.multi) { + return true; + } + return !argEntries.some(([name, values]) => { + return ( + values.length > 0 && + (name === matchingArgName || (matchingArgDef.aliases || []).includes(name)) + ); + }); + }); + + // Sort by whether or not the arg is also the unnamed, then by whether or not the arg name starts + // with the text at the marker, then alphabetically + const comparator = combinedComparator( + unnamedArgComparator, + invokeWithProp( + startsWithComparator(query), + 'name' + ), + invokeWithProp( + alphanumericalComparator, + 'name' + ) + ); + const argDefs = unusedArgDefs.map(([name, arg]) => ({ name, ...arg })).sort(comparator); + + return argDefs.map(argDef => { + return { type: 'argument', text: argDef.name + '=', start, end: end - MARKER.length, argDef }; + }); +} + +function getArgValueSuggestions( + specs: CanvasFunction[], + ast: ExpressionASTWithMeta, + fnIndex: number, + argName: string, + argIndex: number +) { + // Get the list of values from the argument definition + const fn = ast.node.chain[fnIndex].node; + const fnDef = getByAlias(specs, fn.function); + if (!fnDef) { + return []; + } + const argDef = getByAlias(fnDef.args, argName); + if (!argDef) { + return []; + } + + // Get suggestions from the argument definition, including the default + const { start, end, node } = fn.arguments[argName][argIndex]; + if (typeof node !== 'string') { + return []; + } + const query = node.replace(MARKER, ''); + const argOptions = argDef.options ? argDef.options : []; + + let suggestions = [...argOptions]; + + if (argDef.default !== undefined) { + suggestions.push(argDef.default); + } + + suggestions = uniq(suggestions); + + // Filter the list of suggestions by the text at the marker + const filtered = suggestions.filter(option => textMatches(String(option), query)); + + // Sort by whether or not the value starts with the text at the marker, then alphabetically + const comparator = combinedComparator(startsWithComparator(query), alphanumericalComparator); + const sorted = filtered.sort(comparator); + + return sorted.map(value => { + const text = maybeQuote(value) + ' '; + return { start, end: end - MARKER.length, type: 'value', text }; + }); +} + +function textMatches(text: string, query: string): boolean { + return text.toLowerCase().includes(query.toLowerCase().trim()); +} + +function maybeQuote(value: any) { + if (typeof value === 'string') { + if (value.match(/^\{.*\}$/)) { + return value; + } + return `"${value.replace(/"/g, '\\"')}"`; + } + return value; +} + +function prevFnTypeComparator(prevFnType: any) { + return (a: CanvasFunction, b: CanvasFunction): number => { + return ( + (b.context && b.context.types && b.context.types.includes(prevFnType) ? 1 : 0) - + (a.context && a.context.types && a.context.types.includes(prevFnType) ? 1 : 0) + ); + }; +} + +function unnamedArgComparator(a: ArgValue, b: ArgValue): number { + return ( + (b.aliases && b.aliases.includes('_') ? 1 : 0) - (a.aliases && a.aliases.includes('_') ? 1 : 0) + ); +} + +function alphanumericalComparator(a: any, b: any): number { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; +} + +function startsWithComparator(query: string) { + return (a: any, b: any) => + (String(b).startsWith(query) ? 1 : 0) - (String(a).startsWith(query) ? 1 : 0); +} + +type Comparator = (a: T, b: T) => number; + +function combinedComparator(...comparators: Array>): Comparator { + return (a: T, b: T) => + comparators.reduce((acc: number, comparator) => { + if (acc !== 0) { + return acc; + } + return comparator(a, b); + }, 0); +} + +function invokeWithProp< + PropType, + PropName extends string, + ArgType extends { [key in PropName]: PropType }, + FnReturnType +>(fn: (...args: PropType[]) => FnReturnType, prop: PropName): (...args: ArgType[]) => FnReturnType { + return (...args: Array<{ [key in PropName]: PropType }>) => { + return fn(...args.map(arg => arg[prop])); + }; +} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad/index.js index b79e5bba099e73..723b30aac37fa8 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad/index.js @@ -19,7 +19,7 @@ import { getPages, } from '../../state/selectors/workpad'; import { zoomHandlerCreators } from '../../lib/app_handler_creators'; -import { trackCanvasUiMetric } from '../../lib/ui_metric'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; import { LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY } from '../../../common/lib/constants'; import { Workpad as Component } from './workpad'; @@ -56,6 +56,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { if (value === true) { trackCanvasUiMetric( + METRIC_TYPE.COUNT, stateProps.autoplayEnabled ? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY] : LAUNCHED_FULLSCREEN diff --git a/x-pack/legacy/plugins/canvas/server/usage/collector_helpers.ts b/x-pack/legacy/plugins/canvas/server/usage/collector_helpers.ts index d86ebf4b4513e6..784042fb4d94da 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/collector_helpers.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/collector_helpers.ts @@ -9,10 +9,14 @@ * @param cb: callback to do something with a function that has been found */ -import { AST } from '../../types'; +import { ExpressionAST, ExpressionArgAST } from '../../types'; -export function collectFns(ast: AST, cb: (functionName: string) => void) { - if (ast.type === 'expression') { +function isExpression(maybeExpression: ExpressionArgAST): maybeExpression is ExpressionAST { + return typeof maybeExpression === 'object'; +} + +export function collectFns(ast: ExpressionArgAST, cb: (functionName: string) => void) { + if (isExpression(ast)) { ast.chain.forEach(({ function: cFunction, arguments: cArguments }) => { cb(cFunction); diff --git a/x-pack/legacy/plugins/canvas/server/usage/__tests__/custom_element_collector.ts b/x-pack/legacy/plugins/canvas/server/usage/custom_element_collector.test.ts similarity index 71% rename from x-pack/legacy/plugins/canvas/server/usage/__tests__/custom_element_collector.ts rename to x-pack/legacy/plugins/canvas/server/usage/custom_element_collector.test.ts index f75ff9646c06b5..f09bb704b09e37 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/__tests__/custom_element_collector.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/custom_element_collector.test.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { summarizeCustomElements } from '../custom_element_collector'; -import { TelemetryCustomElementDocument } from '../../../types'; +import { summarizeCustomElements } from './custom_element_collector'; +import { TelemetryCustomElementDocument } from '../../types'; function mockCustomElement(...nodeExpressions: string[]): TelemetryCustomElementDocument { return { @@ -21,7 +20,7 @@ function mockCustomElement(...nodeExpressions: string[]): TelemetryCustomElement describe('custom_element_collector.handleResponse', () => { describe('invalid responses', () => { it('returns nothing if no valid hits', () => { - expect(summarizeCustomElements([])).to.eql({}); + expect(summarizeCustomElements([])).toEqual({}); }); it('returns nothing if no valid elements', () => { @@ -31,7 +30,7 @@ describe('custom_element_collector.handleResponse', () => { }, ]; - expect(summarizeCustomElements(customElements)).to.eql({}); + expect(summarizeCustomElements(customElements)).toEqual({}); }); }); @@ -39,10 +38,10 @@ describe('custom_element_collector.handleResponse', () => { const elements = [mockCustomElement(''), mockCustomElement('')]; const data = summarizeCustomElements(elements); - expect(data.custom_elements).to.not.be(null); + expect(data.custom_elements).not.toBe(null); if (data.custom_elements) { - expect(data.custom_elements.count).to.equal(elements.length); + expect(data.custom_elements.count).toEqual(elements.length); } }); @@ -54,10 +53,10 @@ describe('custom_element_collector.handleResponse', () => { const elements = [mockCustomElement(functions1.join('|')), mockCustomElement(...functions2)]; const data = summarizeCustomElements(elements); - expect(data.custom_elements).to.not.be(null); + expect(data.custom_elements).not.toBe(null); if (data.custom_elements) { - expect(data.custom_elements.functions_in_use).to.eql(expectedFunctions); + expect(data.custom_elements.functions_in_use).toEqual(expectedFunctions); } }); @@ -74,12 +73,12 @@ describe('custom_element_collector.handleResponse', () => { ]; const result = summarizeCustomElements(elements); - expect(result.custom_elements).to.not.be(null); + expect(result.custom_elements).not.toBe(null); if (result.custom_elements) { - expect(result.custom_elements.elements.max).to.equal(functionsMax.length); - expect(result.custom_elements.elements.min).to.equal(functionsMin.length); - expect(result.custom_elements.elements.avg).to.equal(avgFunctions); + expect(result.custom_elements.elements.max).toEqual(functionsMax.length); + expect(result.custom_elements.elements.min).toEqual(functionsMin.length); + expect(result.custom_elements.elements.avg).toEqual(avgFunctions); } }); }); diff --git a/x-pack/legacy/plugins/canvas/server/usage/custom_element_collector.ts b/x-pack/legacy/plugins/canvas/server/usage/custom_element_collector.ts index b7cc4391694571..9def60f9c7111c 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/custom_element_collector.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/custom_element_collector.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { fromExpression } from '@kbn/interpreter/common'; import { collectFns } from './collector_helpers'; import { TelemetryCollector } from '../../types'; -import { AST, TelemetryCustomElement, TelemetryCustomElementDocument } from '../../types'; +import { ExpressionAST, TelemetryCustomElement, TelemetryCustomElementDocument } from '../../types'; const CUSTOM_ELEMENT_TYPE = 'canvas-element'; interface CustomElementSearch { @@ -48,7 +48,6 @@ function parseJsonOrNull(maybeJson: string) { /** Calculate statistics about a collection of CustomElement Documents - @param customElements - Array of CustomElement documents @returns Statistics about how Custom Elements are being used */ @@ -76,7 +75,7 @@ export function summarizeCustomElements( parsedContents.map(contents => { contents.selectedNodes.map(node => { - const ast: AST = fromExpression(node.expression) as AST; // TODO: Remove once fromExpression is properly typed + const ast: ExpressionAST = fromExpression(node.expression) as ExpressionAST; // TODO: Remove once fromExpression is properly typed collectFns(ast, (cFunction: string) => { functionSet.add(cFunction); }); diff --git a/x-pack/legacy/plugins/canvas/server/usage/__tests__/workpad_collector.ts b/x-pack/legacy/plugins/canvas/server/usage/workpad_collector.test.ts similarity index 63% rename from x-pack/legacy/plugins/canvas/server/usage/__tests__/workpad_collector.ts rename to x-pack/legacy/plugins/canvas/server/usage/workpad_collector.test.ts index a59eab5c980363..420b785771bfed 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/__tests__/workpad_collector.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/workpad_collector.test.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { summarizeWorkpads } from '../workpad_collector'; -// @ts-ignore Missing local definition -import { workpads } from '../../../__tests__/fixtures/workpads'; +import clonedeep from 'lodash.clonedeep'; +import { summarizeWorkpads } from './workpad_collector'; +import { workpads } from '../../__tests__/fixtures/workpads'; describe('usage collector handle es response data', () => { it('should summarize workpads, pages, and elements', () => { const usage = summarizeWorkpads(workpads); - expect(usage).to.eql({ + expect(usage).toEqual({ workpads: { total: 6, // num workpad documents in .kibana index }, @@ -54,29 +53,12 @@ describe('usage collector handle es response data', () => { }); it('should collect correctly if an expression has null as an argument (possible sub-expression)', () => { - const mockWorkpads = [ - { - name: 'Tweet Data Workpad 1', - id: 'workpad-ae00567f-5510-4d68-b07f-6b1661948e03', - width: 792, - height: 612, - page: 0, - pages: [ - { - elements: [ - { - expression: 'toast butter=null', - }, - ], - }, - ], - '@timestamp': '2018-07-26T02:29:00.964Z', - '@created': '2018-07-25T22:56:31.460Z', - assets: {}, - }, - ]; + const workpad = clonedeep(workpads[0]); + workpad.pages[0].elements[0].expression = 'toast butter=null'; + + const mockWorkpads = [workpad]; const usage = summarizeWorkpads(mockWorkpads); - expect(usage).to.eql({ + expect(usage).toEqual({ workpads: { total: 1 }, pages: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, elements: { total: 1, per_page: { avg: 1, min: 1, max: 1 } }, @@ -85,21 +67,11 @@ describe('usage collector handle es response data', () => { }); it('should fail gracefully if workpad has 0 pages (corrupted workpad)', () => { - const mockWorkpadsCorrupted = [ - { - name: 'Tweet Data Workpad 2', - id: 'workpad-ae00567f-5510-4d68-b07f-6b1661948e03', - width: 792, - height: 612, - page: 0, - pages: [], // pages should never be empty, and *may* prevent the ui from rendering properly - '@timestamp': '2018-07-26T02:29:00.964Z', - '@created': '2018-07-25T22:56:31.460Z', - assets: {}, - }, - ]; + const workpad = clonedeep(workpads[0]); + workpad.pages = []; + const mockWorkpadsCorrupted = [workpad]; const usage = summarizeWorkpads(mockWorkpadsCorrupted); - expect(usage).to.eql({ + expect(usage).toEqual({ workpads: { total: 1 }, pages: { total: 0, per_workpad: { avg: 0, min: 0, max: 0 } }, elements: undefined, @@ -109,6 +81,6 @@ describe('usage collector handle es response data', () => { it('should fail gracefully in general', () => { const usage = summarizeWorkpads([]); - expect(usage).to.eql({}); + expect(usage).toEqual({}); }); }); diff --git a/x-pack/legacy/plugins/canvas/server/usage/workpad_collector.ts b/x-pack/legacy/plugins/canvas/server/usage/workpad_collector.ts index 5f003c6555b6cf..03b2a006e7621e 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/workpad_collector.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/workpad_collector.ts @@ -9,23 +9,10 @@ import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash'; import { fromExpression } from '@kbn/interpreter/common'; import { CANVAS_TYPE } from '../../common/lib/constants'; import { collectFns } from './collector_helpers'; -import { AST, TelemetryCollector } from '../../types'; - -interface Element { - expression: string; -} - -interface Page { - elements: Element[]; -} - -interface Workpad { - pages: Page[]; - [s: string]: any; // Only concerned with the pages here, but allow workpads to have any values -} +import { ExpressionAST, TelemetryCollector, CanvasWorkpad } from '../../types'; interface WorkpadSearch { - [CANVAS_TYPE]: Workpad; + [CANVAS_TYPE]: CanvasWorkpad; } interface WorkpadTelemetry { @@ -61,11 +48,10 @@ interface WorkpadTelemetry { /** Gather statistic about the given workpads - @param workpadDocs a collection of workpad documents @returns Workpad Telemetry Data */ -export function summarizeWorkpads(workpadDocs: Workpad[]): WorkpadTelemetry { +export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetry { const functionSet = new Set(); if (workpadDocs.length === 0) { @@ -87,7 +73,7 @@ export function summarizeWorkpads(workpadDocs: Workpad[]): WorkpadTelemetry { ); const functionCounts = workpad.pages.reduce((accum, page) => { return page.elements.map(element => { - const ast: AST = fromExpression(element.expression) as AST; // TODO: Remove once fromExpression is properly typed + const ast: ExpressionAST = fromExpression(element.expression) as ExpressionAST; // TODO: Remove once fromExpression is properly typed collectFns(ast, cFunction => { functionSet.add(cFunction); }); diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index 35b3a28e1ae0fa..48047c327a6e93 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ExpressionAST } from 'src/plugins/data/common/expressions'; + export interface ElementSpec { name: string; image: string; @@ -49,16 +51,6 @@ export interface CustomElement { content: string; } -export interface AST { - type: string; - chain: Array<{ - function: string; - arguments: { - [s: string]: AST[]; - }; - }>; -} - export interface ElementPosition { /** * distance from the left edge of the page @@ -102,5 +94,5 @@ export interface PositionedElement { /** * AST of the Canvas expression for the element */ - ast: AST; + ast: ExpressionAST; } diff --git a/x-pack/legacy/plugins/code/common/language_server.ts b/x-pack/legacy/plugins/code/common/language_server.ts index 8e094d71d02621..e373f40ab49da2 100644 --- a/x-pack/legacy/plugins/code/common/language_server.ts +++ b/x-pack/legacy/plugins/code/common/language_server.ts @@ -48,8 +48,6 @@ export const CTAGS_SUPPORT_LANGS = [ 'sql', 'tcl', 'typescript', - 'xml', - 'yaml', 'java', 'javascript', ]; diff --git a/x-pack/legacy/plugins/code/public/actions/editor.ts b/x-pack/legacy/plugins/code/public/actions/editor.ts index 146c54fca0fa1d..0baeae10fe93c1 100644 --- a/x-pack/legacy/plugins/code/public/actions/editor.ts +++ b/x-pack/legacy/plugins/code/public/actions/editor.ts @@ -8,17 +8,17 @@ import { Range } from 'monaco-editor'; import { createAction } from 'redux-actions'; import { Hover, Position, TextDocumentPositionParams } from 'vscode-languageserver'; -export interface ReferenceResults { - repos: GroupedRepoReferences[]; +export interface PanelResults { + repos: GroupedRepoResults[]; title: string; } -export interface GroupedRepoReferences { +export interface GroupedRepoResults { repo: string; - files: GroupedFileReferences[]; + files: GroupedFileResults[]; } -export interface GroupedFileReferences { +export interface GroupedFileResults { uri: string; file: string; language: string; @@ -30,8 +30,12 @@ export interface GroupedFileReferences { } export const findReferences = createAction('FIND REFERENCES'); -export const findReferencesSuccess = createAction('FIND REFERENCES SUCCESS'); +export const findReferencesSuccess = createAction('FIND REFERENCES SUCCESS'); export const findReferencesFailed = createAction('FIND REFERENCES ERROR'); -export const closeReferences = createAction('CLOSE REFERENCES'); +export const closePanel = createAction('CLOSE PANEL'); export const hoverResult = createAction('HOVER RESULT'); export const revealPosition = createAction('REVEAL POSITION'); + +export const findDefinitions = createAction('FIND DEFINITIONS'); +export const findDefinitionsSuccess = createAction('FIND DEFINITIONS SUCCESS'); +export const findDefinitionsFailed = createAction('FIND DEFINITIONS ERROR'); diff --git a/x-pack/legacy/plugins/code/public/components/editor/editor.tsx b/x-pack/legacy/plugins/code/public/components/editor/editor.tsx index 9eb57e54338615..6ea79e4e2282fc 100644 --- a/x-pack/legacy/plugins/code/public/components/editor/editor.tsx +++ b/x-pack/legacy/plugins/code/public/components/editor/editor.tsx @@ -9,9 +9,9 @@ import { editor as editorInterfaces, IDisposable } from 'monaco-editor'; import React from 'react'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { Hover, Position, TextDocumentPositionParams } from 'vscode-languageserver-protocol'; +import { Hover, Position } from 'vscode-languageserver-protocol'; import { GitBlame } from '../../../common/git_blame'; -import { closeReferences, FetchFileResponse, findReferences, hoverResult } from '../../actions'; +import { closePanel, FetchFileResponse, hoverResult } from '../../actions'; import { MainRouteParams } from '../../common/types'; import { BlameWidget } from '../../monaco/blame/blame_widget'; import { monaco } from '../../monaco/monaco'; @@ -24,8 +24,7 @@ import { ReferencesPanel } from './references_panel'; import { encodeRevisionString } from '../../../common/uri_util'; export interface EditorActions { - closeReferences(changeUrl: boolean): void; - findReferences(params: TextDocumentPositionParams): void; + closePanel(changeUrl: boolean): void; hoverResult(hover: Hover): void; } @@ -33,10 +32,10 @@ interface Props { hidden?: boolean; file?: FetchFileResponse; revealPosition?: Position; - isReferencesOpen: boolean; - isReferencesLoading: boolean; - references: any[]; - referencesTitle: string; + panelShowing: boolean; + isPanelLoading: boolean; + panelContents: any[]; + panelTitle: string; hover?: Hover; refUrl?: string; blames: GitBlame[]; @@ -237,12 +236,12 @@ export class EditorComponent extends React.Component { private renderReferences() { return ( - this.props.isReferencesOpen && ( + this.props.panelShowing && ( this.props.closeReferences(true)} - references={this.props.references} - isLoading={this.props.isReferencesLoading} - title={this.props.referencesTitle} + onClose={() => this.props.closePanel(true)} + references={this.props.panelContents} + isLoading={this.props.isPanelLoading} + title={this.props.panelTitle} refUrl={this.props.refUrl} /> ) @@ -252,10 +251,10 @@ export class EditorComponent extends React.Component { const mapStateToProps = (state: RootState) => ({ file: state.file.file, - isReferencesOpen: state.editor.showing, - isReferencesLoading: state.editor.loading, - references: state.editor.references, - referencesTitle: state.editor.referencesTitle, + panelShowing: state.editor.panelShowing, + isPanelLoading: state.editor.loading, + panelContents: state.editor.panelContents, + panelTitle: state.editor.panelTitle, hover: state.editor.hover, refUrl: refUrlSelector(state), revealPosition: state.editor.revealPosition, @@ -263,8 +262,7 @@ const mapStateToProps = (state: RootState) => ({ }); const mapDispatchToProps = { - closeReferences, - findReferences, + closePanel, hoverResult, }; diff --git a/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx b/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx index fa6635c543378e..219c5e837155c3 100644 --- a/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx +++ b/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx @@ -18,14 +18,14 @@ import { IPosition } from 'monaco-editor'; import queryString from 'querystring'; import React from 'react'; import { parseSchema } from '../../../common/uri_util'; -import { GroupedFileReferences, GroupedRepoReferences } from '../../actions'; +import { GroupedFileResults, GroupedRepoResults } from '../../actions'; import { history } from '../../utils/url'; import { CodeBlock } from '../codeblock/codeblock'; interface Props { isLoading: boolean; title: string; - references: GroupedRepoReferences[]; + references: GroupedRepoResults[]; refUrl?: string; onClose(): void; } @@ -85,12 +85,12 @@ export class ReferencesPanel extends React.Component { } private renderGroupByRepo() { - return this.props.references.map((ref: GroupedRepoReferences) => { + return this.props.references.map((ref: GroupedRepoResults) => { return this.renderReferenceRepo(ref); }); } - private renderReferenceRepo({ repo, files }: GroupedRepoReferences) { + private renderReferenceRepo({ repo, files }: GroupedRepoResults) { const [org, name] = repo.split('/').slice(1); const buttonContent = ( @@ -112,7 +112,7 @@ export class ReferencesPanel extends React.Component { ); } - private renderReference(file: GroupedFileReferences) { + private renderReference(file: GroupedFileResults) { const key = `${file.uri}`; const lineNumberFn = (l: number) => { return file.lineNumbers[l - 1]; diff --git a/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts b/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts index f03c914be3f315..7ef9dfc88b904e 100644 --- a/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts +++ b/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import url from 'url'; +import queryString from 'querystring'; import { DetailSymbolInformation } from '@elastic/lsp-extension'; import { npStart } from 'ui/new_platform'; import { Location } from 'vscode-languageserver-types'; import { monaco } from '../monaco'; import { LspRestClient, TextDocumentMethods } from '../../../common/lsp_client'; +import { parseSchema } from '../../../common/uri_util'; +import { history } from '../../utils/url'; export const definitionProvider: monaco.languages.DefinitionProvider = { async provideDefinition( @@ -41,6 +45,20 @@ export const definitionProvider: monaco.languages.DefinitionProvider = { return []; } + function openDefinitionsPanel() { + if (model && position) { + const { uri } = parseSchema(model.uri.toString()); + const refUrl = `git:/${uri}!L${position.lineNumber - 1}:${position.column - 1}`; + const queries = url.parse(history.location.search, true).query; + const query = queryString.stringify({ + ...queries, + tab: 'definitions', + refUrl, + }); + history.push(`${uri}?${query}`); + } + } + const result = await lspMethods.edefinition.send({ position: { line: position.lineNumber - 1, @@ -52,20 +70,19 @@ export const definitionProvider: monaco.languages.DefinitionProvider = { }); if (result) { - const locations = result.filter(l => l.location !== undefined).map(l => l.location!); - if (locations.length > 0) { - return locations.map(handleLocation); + if (result.length > 1) { + openDefinitionsPanel(); + return []; } else { - let qnameResults: monaco.languages.Location[] = []; - for (const l of result) { - if (l.qname) { - qnameResults = qnameResults.concat(await handleQname(l.qname)); - } + const l = result[0]; + const location = l.location; + if (location) { + return [handleLocation(location)]; + } else if (l.qname) { + return await handleQname(l.qname); } - return qnameResults; } } - return []; }, }; diff --git a/x-pack/legacy/plugins/code/public/reducers/editor.ts b/x-pack/legacy/plugins/code/public/reducers/editor.ts index d1104f5144673f..ed7dc780b4407e 100644 --- a/x-pack/legacy/plugins/code/public/reducers/editor.ts +++ b/x-pack/legacy/plugins/code/public/reducers/editor.ts @@ -7,73 +7,100 @@ import produce from 'immer'; import { handleActions, Action } from 'redux-actions'; import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'; import { - closeReferences, + closePanel, findReferences, findReferencesFailed, findReferencesSuccess, - GroupedRepoReferences, + GroupedRepoResults, hoverResult, revealPosition, - ReferenceResults, + PanelResults, + findDefinitions, + findDefinitionsSuccess, + findDefinitionsFailed, } from '../actions'; export interface EditorState { loading: boolean; - showing: boolean; - references: GroupedRepoReferences[]; + panelShowing: 'references' | 'definitions' | undefined; + panelContents: GroupedRepoResults[]; hover?: Hover; currentHover?: Hover; refPayload?: TextDocumentPositionParams; revealPosition?: Position; - referencesTitle: string; + panelTitle: string; } const initialState: EditorState = { loading: false, - showing: false, - references: [], - referencesTitle: '', + panelShowing: undefined, + panelContents: [], + panelTitle: '', }; -type EditorPayload = ReferenceResults & Hover & TextDocumentPositionParams & Position & string; +type EditorPayload = PanelResults & Hover & TextDocumentPositionParams & Position & string; + +function panelInit(draft: EditorState, action: Action) { + draft.refPayload = action.payload!; + draft.loading = true; + draft.panelContents = initialState.panelContents; + draft.panelTitle = initialState.panelTitle; +} + +function panelSuccess(draft: EditorState, action: Action) { + const { title, repos } = action.payload!; + draft.panelContents = repos; + draft.panelTitle = title; + draft.loading = false; +} +function panelFailed(draft: EditorState) { + draft.panelContents = []; + draft.loading = false; + delete draft.refPayload; +} export const editor = handleActions( { [String(findReferences)]: (state, action: Action) => - produce(state, draft => { - draft.refPayload = action.payload; - draft.showing = true; - draft.loading = true; - draft.references = initialState.references; - draft.hover = state.currentHover; - draft.referencesTitle = initialState.referencesTitle; + produce(state, (draft: EditorState) => { + panelInit(draft, action); + draft.panelShowing = 'references'; }), - [String(findReferencesSuccess)]: (state, action: any) => - produce(state, draft => { - const { title, repos } = action.payload; - draft.references = repos; - draft.referencesTitle = title; - draft.loading = false; + [String(findReferencesSuccess)]: (state, action: Action) => + produce(state, (draft: EditorState) => { + panelSuccess(draft, action); }), [String(findReferencesFailed)]: state => - produce(state, draft => { - draft.references = []; - draft.loading = false; - draft.refPayload = undefined; + produce(state, (draft: EditorState) => { + panelFailed(draft); + }), + [String(findDefinitions)]: (state, action: Action) => + produce(state, (draft: EditorState) => { + panelInit(draft, action); + draft.panelShowing = 'definitions'; + }), + [String(findDefinitionsSuccess)]: (state, action: Action) => + produce(state, (draft: EditorState) => { + panelSuccess(draft, action); + }), + [String(findDefinitionsFailed)]: state => + produce(state, (draft: EditorState) => { + panelFailed(draft); }), - [String(closeReferences)]: state => - produce(state, draft => { - draft.showing = false; + [String(closePanel)]: state => + produce(state, (draft: EditorState) => { + draft.panelShowing = undefined; draft.loading = false; - draft.refPayload = undefined; - draft.references = []; + delete draft.refPayload; + draft.panelContents = []; + draft.panelTitle = ''; }), [String(hoverResult)]: (state, action: Action) => - produce(state, draft => { - draft.currentHover = action.payload; + produce(state, (draft: EditorState) => { + draft.currentHover = action.payload!; }), [String(revealPosition)]: (state, action: Action) => - produce(state, draft => { - draft.revealPosition = action.payload; + produce(state, (draft: EditorState) => { + draft.revealPosition = action.payload!; }), }, initialState diff --git a/x-pack/legacy/plugins/code/public/sagas/editor.ts b/x-pack/legacy/plugins/code/public/sagas/editor.ts index 437846139d273b..1f6ed5d79f1ce3 100644 --- a/x-pack/legacy/plugins/code/public/sagas/editor.ts +++ b/x-pack/legacy/plugins/code/public/sagas/editor.ts @@ -13,7 +13,6 @@ import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; import { parseGoto, parseLspUrl, toCanonicalUrl } from '../../common/uri_util'; import { FileTree } from '../../model'; import { - closeReferences, fetchFile, FetchFileResponse, fetchRepoTree, @@ -26,6 +25,10 @@ import { revealPosition, fetchRepos, turnOnDefaultRepoScope, + findDefinitions, + findDefinitionsSuccess, + findDefinitionsFailed, + closePanel, } from '../actions'; import { loadRepo, loadRepoFailed, loadRepoSuccess } from '../actions/status'; import { PathTypes } from '../common/types'; @@ -55,17 +58,35 @@ function* handleReferences(action: Action) { } } +function* handleDefinitions(action: Action) { + try { + const params: TextDocumentPositionParams = action.payload!; + const { title, files } = yield call(requestFindDefinitions, params); + const repos = Object.keys(files).map((repo: string) => ({ repo, files: files[repo] })); + yield put(findDefinitionsSuccess({ title, repos })); + } catch (error) { + yield put(findDefinitionsFailed(error)); + } +} + function requestFindReferences(params: TextDocumentPositionParams) { return npStart.core.http.post(`/api/code/lsp/findReferences`, { body: JSON.stringify(params), }); } +function requestFindDefinitions(params: TextDocumentPositionParams) { + return npStart.core.http.post(`/api/code/lsp/findDefinitions`, { + body: JSON.stringify(params), + }); +} + export function* watchLspMethods() { yield takeLatest(String(findReferences), handleReferences); + yield takeLatest(String(findDefinitions), handleDefinitions); } -function* handleCloseReferences(action: Action) { +function* handleClosePanel(action: Action) { if (action.payload) { const search = yield select(urlQueryStringSelector); const { pathname } = history.location; @@ -86,7 +107,7 @@ function* handleCloseReferences(action: Action) { } export function* watchCloseReference() { - yield takeLatest(String(closeReferences), handleCloseReferences); + yield takeLatest(String(closePanel), handleClosePanel); } function* handleReference(url: string) { @@ -107,6 +128,24 @@ function* handleReference(url: string) { } } +function* openDefinitions(url: string) { + const refUrl = yield select(refUrlSelector); + if (refUrl === url) { + return; + } + const { uri, position, schema, repoUri, file, revision } = parseLspUrl(url); + if (uri && position) { + yield put( + findDefinitions({ + textDocument: { + uri: toCanonicalUrl({ revision, schema, repoUri, file }), + }, + position, + }) + ); + } +} + function* handleFile(repoUri: string, file: string, revision: string) { const response: FetchFileResponse = yield select(fileSelector); const payload = response && response.payload; @@ -171,10 +210,13 @@ function* handleMainRouteChange(action: Action) { const { tab, refUrl } = queryParams; if (tab === 'references' && refUrl) { yield call(handleReference, decodeURIComponent(refUrl as string)); + } else if (tab === 'definitions' && refUrl) { + yield call(openDefinitions, decodeURIComponent(refUrl as string)); } else { - yield put(closeReferences(false)); + yield put(closePanel(false)); } } + yield call(handleFile, repoUri, file, revision); const commits = yield select((state: RootState) => state.revision.treeCommits[file]); if (commits === undefined) { diff --git a/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts index cf861f100921d8..d355b0f5b7398f 100644 --- a/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ChildProcess } from 'child_process'; -import fs from 'fs'; import { ResponseError } from 'vscode-jsonrpc'; import { LanguageServerStartFailed } from '../../common/lsp_error_codes'; import { Logger } from '../log'; @@ -14,16 +12,14 @@ import { LoggerFactory } from '../utils/log_factory'; import { ILanguageServerLauncher } from './language_server_launcher'; import { LanguageServerProxy } from './proxy'; import { RequestExpander } from './request_expander'; +import { ControlledProgram } from './process/controlled_program'; let seqNo = 1; -const OOM_SCORE_ADJ = 667; -const OOM_ADJ = 10; - export abstract class AbstractLauncher implements ILanguageServerLauncher { running: boolean = false; private currentPid: number = -1; - private child: ChildProcess | null = null; + private child: ControlledProgram | null = null; private startTime: number = -1; private proxyConnected: boolean = false; protected readonly log: Logger; @@ -41,7 +37,6 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { public async launch(builtinWorkspace: boolean, maxWorkspace: number, installationPath: string) { const port = await this.getPort(); const log: Logger = this.log; - let child: ChildProcess; const proxy = new LanguageServerProxy(port, this.targetHost, log); if (this.options.lsp.detach) { log.debug('Detach mode, expected language server launch externally'); @@ -56,7 +51,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { } }); } else { - child = await this.doSpawnProcess(installationPath, port, log); + const child = await this.doSpawnProcess(installationPath, port, log); this.onProcessExit(child, () => { if (!proxy.isClosed) this.reconnect(proxy, installationPath); }); @@ -69,12 +64,13 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { }, 1000); } else if (this.child) { log.info('proxy closed, kill process'); - await this.killProcess(this.child); + await this.killProcess(child); } }); + this.child = child; } proxy.onExit(() => { - log.debug('proxy exited, is the process running? ' + this.running); + log.debug('proxy exited, is the program running? ' + this.running); if (this.child && this.running) { const p = this.child!; this.killProcess(p); @@ -96,9 +92,9 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { return this.createExpander(proxy, builtinWorkspace, maxWorkspace); } - private onProcessExit(child: ChildProcess, reconnectFn: () => void) { + private onProcessExit(child: ControlledProgram, reconnectFn: () => void) { const pid = child.pid; - child.on('exit', () => { + child.onExit(() => { if (this.currentPid === pid) { this.running = false; // if the process exited before proxy connected, then we reconnect @@ -122,19 +118,19 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { public async reconnect( proxy: LanguageServerProxy, installationPath: string, - child?: ChildProcess + child?: ControlledProgram ) { this.log.debug('reconnecting'); if (this.options.lsp.detach) { this.startConnect(proxy); } else { const processExpired = () => Date.now() - this.startTime > this.startupTimeout; - if (child && !child.killed && !processExpired()) { + if (child && !child.killed() && !processExpired()) { this.startConnect(proxy); } else { if (this.spawnTimes < this.maxRespawn) { if (child && this.running) { - this.log.debug('killing the old process.'); + this.log.debug('killing the old program.'); await this.killProcess(child); } const port = await this.getPort(); @@ -149,7 +145,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { ); this.launchReject!(ServerStartFailed); proxy.setError(ServerStartFailed); - this.log.warn(`spawned process ${this.spawnTimes} times, mark this proxy unusable.`); + this.log.warn(`spawned program ${this.spawnTimes} times, mark this proxy unusable.`); } } } @@ -159,33 +155,13 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { installationPath: string, port: number, log: Logger - ): Promise { - this.log.debug('spawn process'); + ): Promise { + this.log.debug('start program'); const child = await this.spawnProcess(installationPath, port, log); - const pid = child.pid; - this.currentPid = pid; - this.log.debug('spawned a child process ' + pid); + this.currentPid = child.pid; this.spawnTimes += 1; this.startTime = Date.now(); this.running = true; - if (this.options.lsp.oomScoreAdj && process.platform === 'linux') { - try { - // clone form https://github.com/elastic/ml-cpp/blob/4dd90fa93338667b681364657222715f81c9868a/lib/core/CProcessPriority_Linux.cc - fs.writeFileSync(`/proc/${pid}/oom_score_adj`, `${OOM_SCORE_ADJ}\n`); - this.log.debug(`wrote oom_score_adj of process ${pid} to ${OOM_SCORE_ADJ}`); - } catch (e) { - this.log.warn(e); - try { - fs.writeFileSync(`/proc/${pid}/oom_adj`, `${OOM_ADJ}\n`); - this.log.debug(`wrote oom_adj of process ${pid} to ${OOM_ADJ}`); - } catch (err) { - this.log.warn( - 'write oom_score_adj and oom_adj file both failed, reduce priority not working' - ); - this.log.warn(err); - } - } - } return child; } @@ -198,6 +174,8 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { /** * await for proxy connected, create a request expander * @param proxy + * @param builtinWorkspace + * @param maxWorkspace */ abstract createExpander( proxy: LanguageServerProxy, @@ -209,25 +187,23 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { installationPath: string, port: number, log: Logger - ): Promise; + ): Promise; - protected killProcess(child: ChildProcess) { - if (!child.killed) { + protected killProcess(child: ControlledProgram) { + if (!child.killed()) { return new Promise((resolve, reject) => { // if not killed within 1s const t = setTimeout(reject, 1000); - child.on('exit', () => { + child.onExit(() => { clearTimeout(t); resolve(true); }); - child.kill(); - this.log.info('killed process ' + child.pid); + child.kill(false); }) .catch(() => { // force kill - child.kill('SIGKILL'); - this.log.info('force killed process ' + child.pid); - return child.killed; + child.kill(true); + return child.killed(); }) .finally(() => { if (this.currentPid === child.pid) this.running = false; diff --git a/x-pack/legacy/plugins/code/server/lsp/ctags_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/ctags_launcher.ts index 70aa4ff0253c50..f03e206dbb7af0 100644 --- a/x-pack/legacy/plugins/code/server/lsp/ctags_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/ctags_launcher.ts @@ -5,13 +5,13 @@ */ import getPort from 'get-port'; -import { spawn } from 'child_process'; import { ServerOptions } from '../server_options'; import { LoggerFactory } from '../utils/log_factory'; import { LanguageServerProxy } from './proxy'; import { Logger } from '../log'; import { RequestExpander } from './request_expander'; import { AbstractLauncher } from './abstract_launcher'; +import { EmbedCtagServer } from './process/embed_ctag_server'; const CTAGS_LANG_DETACH_PORT = 2092; export class CtagsLauncher extends AbstractLauncher { @@ -47,16 +47,8 @@ export class CtagsLauncher extends AbstractLauncher { } async spawnProcess(installationPath: string, port: number, log: Logger) { - const p = spawn(process.execPath, [installationPath, `--socket=${port.toString()}`], { - detached: false, - stdio: 'pipe', - }); - p.stdout.on('data', data => { - log.stdout(data.toString()); - }); - p.stderr.on('data', data => { - log.stderr(data.toString()); - }); - return p; + const embed = new EmbedCtagServer(port, log); + await embed.start(); + return embed; } } diff --git a/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts index c5333185a200bf..7c4e956537564f 100644 --- a/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts @@ -16,6 +16,7 @@ import { LoggerFactory } from '../utils/log_factory'; import { AbstractLauncher } from './abstract_launcher'; import { LanguageServerProxy } from './proxy'; import { InitializeOptions, RequestExpander } from './request_expander'; +import { ExternalProgram } from './process/external_program'; const GO_LANG_DETACH_PORT = 2091; @@ -131,6 +132,6 @@ export class GoServerLauncher extends AbstractLauncher { log.info( `Launch Go Language Server at port ${port.toString()}, pid:${p.pid}, GOROOT:${goRoot}` ); - return p; + return new ExternalProgram(p, this.options, log); } } diff --git a/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts index 82ba16db15ac14..fd170c7518182d 100644 --- a/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts @@ -16,6 +16,7 @@ import { LoggerFactory } from '../utils/log_factory'; import { AbstractLauncher } from './abstract_launcher'; import { LanguageServerProxy } from './proxy'; import { InitializeOptions, RequestExpander } from './request_expander'; +import { ExternalProgram } from './process/external_program'; const JAVA_LANG_DETACH_PORT = 2090; @@ -186,7 +187,7 @@ export class JavaLauncher extends AbstractLauncher { p.pid }, JAVA_HOME:${javaHomePath}` ); - return p; + return new ExternalProgram(p, this.options, log); } // TODO(pcxu): run /usr/libexec/java_home to get all java homes for macOS diff --git a/x-pack/legacy/plugins/code/server/lsp/process/controlled_program.ts b/x-pack/legacy/plugins/code/server/lsp/process/controlled_program.ts new file mode 100644 index 00000000000000..228d9ae9dcc2df --- /dev/null +++ b/x-pack/legacy/plugins/code/server/lsp/process/controlled_program.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. + */ + +export interface ControlledProgram { + readonly pid: number; + + kill(force: boolean): void; + + onExit(callback: () => void): void; + + killed(): boolean; +} diff --git a/x-pack/legacy/plugins/code/server/lsp/process/embed_ctag_server.ts b/x-pack/legacy/plugins/code/server/lsp/process/embed_ctag_server.ts new file mode 100644 index 00000000000000..abed0cbd79a85e --- /dev/null +++ b/x-pack/legacy/plugins/code/server/lsp/process/embed_ctag_server.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as lsp from 'vscode-languageserver'; +import { IConnection } from 'vscode-languageserver'; +import { createLspConnection } from '@elastic/ctags-langserver/lib/lsp-connection'; +import { Logger } from '../../log'; +import { EmbedProgram } from './embed_program'; + +export class EmbedCtagServer extends EmbedProgram { + private connection: IConnection | null = null; + constructor(readonly port: number, log: Logger) { + super(log); + } + + async stop(): Promise { + if (this.connection) { + this.connection.dispose(); + } + } + + async start(): Promise { + this.connection = createLspConnection({ + ctagsPath: '', + showMessageLevel: lsp.MessageType.Warning, + socketPort: this.port, + }); + this.connection.listen(); + } +} diff --git a/x-pack/legacy/plugins/code/server/lsp/process/embed_program.ts b/x-pack/legacy/plugins/code/server/lsp/process/embed_program.ts new file mode 100644 index 00000000000000..6870312c0674dc --- /dev/null +++ b/x-pack/legacy/plugins/code/server/lsp/process/embed_program.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EventEmitter } from 'events'; +import { ControlledProgram } from './controlled_program'; +import { Logger } from '../../log'; + +let globalPid = 0; + +export abstract class EmbedProgram implements ControlledProgram { + private eventEmitter = new EventEmitter(); + private _killed: boolean = false; + protected constructor(readonly log: Logger) { + this.pid = globalPid++; + this.start().then( + () => { + this.log.debug('started embed program: ' + this.pid); + }, + err => { + this.log.error('embed program launch failed.'); + this.log.debug(err); + } + ); + } + + readonly pid: number; + + kill(force?: boolean): void { + this.stop().then(() => { + this.log.debug('embed program stopped'); + this._killed = true; + this.eventEmitter.emit('exit'); + }); + } + + killed(): boolean { + return this._killed; + } + + onExit(callback: () => void) { + this.eventEmitter.on('exit', callback); + } + + abstract async stop(): Promise; + abstract async start(): Promise; +} diff --git a/x-pack/legacy/plugins/code/server/lsp/process/external_program.ts b/x-pack/legacy/plugins/code/server/lsp/process/external_program.ts new file mode 100644 index 00000000000000..5e13372c3518c2 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/lsp/process/external_program.ts @@ -0,0 +1,62 @@ +/* + * 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 fs from 'fs'; +import { ChildProcess } from 'child_process'; +import { ControlledProgram } from './controlled_program'; +import { ServerOptions } from '../../server_options'; +import { Logger } from '../../log'; + +const OOM_SCORE_ADJ = 667; +const OOM_ADJ = 10; + +export class ExternalProgram implements ControlledProgram { + constructor(readonly child: ChildProcess, readonly options: ServerOptions, readonly log: Logger) { + this.pid = child.pid; + if (this.options.lsp.oomScoreAdj && process.platform === 'linux') { + this.adjustOom(this.pid); + } + this.log.debug('spawned a child process ' + this.pid); + } + + kill(force: boolean = false): void { + if (force) { + this.child.kill('SIGKILL'); + this.log.info('force killed process ' + this.pid); + } else { + this.child.kill(); + this.log.info('killed process ' + this.pid); + } + } + + killed(): boolean { + return this.child.killed; + } + + onExit(callback: () => void) { + this.child.on('exit', callback); + } + + readonly pid: number; + + private adjustOom(pid: number) { + try { + fs.writeFileSync(`/proc/${pid}/oom_score_adj`, `${OOM_SCORE_ADJ}\n`); + this.log.debug(`wrote oom_score_adj of process ${pid} to ${OOM_SCORE_ADJ}`); + } catch (e) { + this.log.warn(e); + try { + fs.writeFileSync(`/proc/${pid}/oom_adj`, `${OOM_ADJ}\n`); + this.log.debug(`wrote oom_adj of process ${pid} to ${OOM_ADJ}`); + } catch (err) { + this.log.warn( + 'write oom_score_adj and oom_adj file both failed, reduce priority not working' + ); + this.log.warn(err); + } + } + } +} diff --git a/x-pack/legacy/plugins/code/server/lsp/ts_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/ts_launcher.ts index ce7a07122889d0..9941f43dd55a67 100644 --- a/x-pack/legacy/plugins/code/server/lsp/ts_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/ts_launcher.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ChildProcess, spawn } from 'child_process'; +import { spawn } from 'child_process'; import getPort from 'get-port'; import { resolve } from 'path'; import { Logger } from '../log'; @@ -13,6 +13,8 @@ import { LoggerFactory } from '../utils/log_factory'; import { AbstractLauncher } from './abstract_launcher'; import { LanguageServerProxy } from './proxy'; import { InitializeOptions, RequestExpander } from './request_expander'; +import { ExternalProgram } from './process/external_program'; +import { ControlledProgram } from './process/controlled_program'; const TS_LANG_DETACH_PORT = 2089; @@ -51,7 +53,11 @@ export class TypescriptServerLauncher extends AbstractLauncher { this.log ); } - async spawnProcess(installationPath: string, port: number, log: Logger): Promise { + async spawnProcess( + installationPath: string, + port: number, + log: Logger + ): Promise { const p = spawn(process.execPath, [installationPath, '-p', port.toString(), '-c', '1'], { detached: false, stdio: 'pipe', @@ -63,6 +69,6 @@ export class TypescriptServerLauncher extends AbstractLauncher { p.stderr.on('data', data => { log.stderr(data.toString()); }); - return p; + return new ExternalProgram(p, this.options, log); } } diff --git a/x-pack/legacy/plugins/code/server/routes/install.ts b/x-pack/legacy/plugins/code/server/routes/install.ts index fb1ea08849b59b..87f5f049fc0fa2 100644 --- a/x-pack/legacy/plugins/code/server/routes/install.ts +++ b/x-pack/legacy/plugins/code/server/routes/install.ts @@ -16,9 +16,9 @@ import { Endpoint } from '../distributed/resource_locator'; export function installRoute(router: CodeServerRouter, codeServices: CodeServices) { const lspService = codeServices.serviceFor(LspServiceDefinition); const kibanaVersion = router.server.config().get('pkg.version') as string; - const status = (endpoint: Endpoint, def: LanguageServerDefinition) => ({ + const status = async (endpoint: Endpoint, def: LanguageServerDefinition) => ({ name: def.name, - status: lspService.languageServerStatus(endpoint, { langName: def.name }), + status: await lspService.languageServerStatus(endpoint, { langName: def.name }), version: def.version, build: def.build, languages: def.languages, @@ -32,7 +32,9 @@ export function installRoute(router: CodeServerRouter, codeServices: CodeService path: '/api/code/install', async handler(req: RequestFacade) { const endpoint = await codeServices.locate(req, ''); - return enabledLanguageServers(router.server).map(def => status(endpoint, def)); + return await Promise.all( + enabledLanguageServers(router.server).map(def => status(endpoint, def)) + ); }, method: 'GET', }); @@ -44,7 +46,7 @@ export function installRoute(router: CodeServerRouter, codeServices: CodeService const def = enabledLanguageServers(router.server).find(d => d.name === name); const endpoint = await codeServices.locate(req, ''); if (def) { - return status(endpoint, def); + return await status(endpoint, def); } else { return Boom.notFound(`language server ${name} not found.`); } diff --git a/x-pack/legacy/plugins/code/server/routes/lsp.ts b/x-pack/legacy/plugins/code/server/routes/lsp.ts index edeefe8f9808d8..2d37620207dec5 100644 --- a/x-pack/legacy/plugins/code/server/routes/lsp.ts +++ b/x-pack/legacy/plugins/code/server/routes/lsp.ts @@ -5,11 +5,10 @@ */ import Boom from 'boom'; -import { groupBy, last } from 'lodash'; import { ResponseError } from 'vscode-jsonrpc'; import { ResponseMessage } from 'vscode-jsonrpc/lib/messages'; -import { Location } from 'vscode-languageserver-types'; +import { SymbolLocator } from '@elastic/lsp-extension'; import { LanguageServerStartFailed, ServerNotInitialized, @@ -17,22 +16,16 @@ import { } from '../../common/lsp_error_codes'; import { parseLspUrl } from '../../common/uri_util'; import { Logger } from '../log'; -import { CTAGS, GO } from '../lsp/language_servers'; import { SymbolSearchClient } from '../search'; import { CodeServerRouter } from '../security'; import { ServerOptions } from '../server_options'; -import { - expandRanges, - extractSourceContent, - LineMapping, - mergeRanges, -} from '../utils/composite_source_merger'; -import { detectLanguage } from '../utils/detect_language'; + import { EsClientWithRequest } from '../utils/esclient_with_request'; import { promiseTimeout } from '../utils/timeout'; import { RequestFacade, ResponseToolkitFacade } from '../..'; import { CodeServices } from '../distributed/code_services'; import { GitServiceDefinition, LspServiceDefinition } from '../distributed/apis'; +import { findTitleFromHover, groupFiles } from '../utils/lsp_utils'; const LANG_SERVER_ERROR = 'language server error'; @@ -44,6 +37,7 @@ export function lspRoute( const log = new Logger(server.server); const lspService = codeServices.serviceFor(LspServiceDefinition); const gitService = codeServices.serviceFor(GitServiceDefinition); + server.route({ path: '/api/code/lsp/textDocument/{method}', async handler(req: RequestFacade, h: ResponseToolkitFacade) { @@ -95,6 +89,52 @@ export function lspRoute( method: 'POST', }); + server.route({ + path: '/api/code/lsp/findDefinitions', + method: 'POST', + async handler(req: RequestFacade, h: ResponseToolkitFacade) { + // @ts-ignore + const { textDocument, position } = req.payload; + const { uri } = textDocument; + const endpoint = await codeServices.locate(req, parseLspUrl(uri).repoUri); + const response: ResponseMessage = await promiseTimeout( + serverOptions.lsp.requestTimeoutMs, + lspService.sendRequest(endpoint, { + method: `textDocument/edefinition`, + params: { textDocument: { uri }, position }, + timeoutForInitializeMs: 1000, + }) + ); + const hover = await lspService.sendRequest(endpoint, { + method: 'textDocument/hover', + params: { + textDocument: { uri }, + position, + }, + }); + const title: string = await findTitleFromHover(hover, uri, position); + const symbolSearchClient = new SymbolSearchClient(new EsClientWithRequest(req), log); + + const locators = response.result as SymbolLocator[]; + const locations = []; + for (const locator of locators) { + if (locator.location) { + locations.push(locator.location); + } else if (locator.qname) { + const searchResults = await symbolSearchClient.findByQname(req.params.qname); + for (const symbol of searchResults.symbols) { + locations.push(symbol.symbolInformation.location); + } + } + } + const files = await groupFiles(locations, async loc => { + const ep = await codeServices.locate(req, loc.uri); + return await gitService.blob(ep, loc); + }); + return { title, files, uri, position }; + }, + }); + server.route({ path: '/api/code/lsp/findReferences', method: 'POST', @@ -119,77 +159,12 @@ export function lspRoute( position, }, }); - let title: string; - if (hover.result && hover.result.contents) { - if (Array.isArray(hover.result.contents)) { - const content = hover.result.contents[0]; - title = hover.result.contents[0].value; - const lang = await detectLanguage(uri.replace('file://', '')); - // TODO(henrywong) Find a gernal approach to construct the reference title. - if (content.kind) { - // The format of the hover result is 'MarkupContent', extract appropriate pieces as the references title. - if (GO.languages.includes(lang)) { - title = title.substring(title.indexOf('```go\n') + 5, title.lastIndexOf('\n```')); - if (title.includes('{\n')) { - title = title.substring(0, title.indexOf('{\n')); - } - } - } else if (CTAGS.languages.includes(lang)) { - // There are language servers may provide hover results with markdown syntax, like ctags-langserver, - // extract the plain text. - if (title.substring(0, 2) === '**' && title.includes('**\n')) { - title = title.substring(title.indexOf('**\n') + 3); - } - } - } else { - title = hover.result.contents as 'string'; - } - } else { - title = last(uri.toString().split('/')) + `(${position.line}, ${position.character})`; - } - const files = []; - const groupedLocations = groupBy(response.result as Location[], 'uri'); - for (const url of Object.keys(groupedLocations)) { - const { repoUri, revision, file } = parseLspUrl(url)!; - const ep = await codeServices.locate(req, repoUri); - const locations: Location[] = groupedLocations[url]; - const lines = locations.map(l => ({ - startLine: l.range.start.line, - endLine: l.range.end.line, - })); - const ranges = expandRanges(lines, 1); - const mergedRanges = mergeRanges(ranges); - const blob = await gitService.blob(ep, { uri: repoUri, path: file!, revision }); - if (blob.content) { - const source = blob.content.split('\n'); - const language = blob.lang; - const lineMappings = new LineMapping(); - const code = extractSourceContent(mergedRanges, source, lineMappings).join('\n'); - const lineNumbers = lineMappings.toStringArray(); - const highlights = locations.map(l => { - const { start, end } = l.range; - const startLineNumber = lineMappings.lineNumber(start.line); - const endLineNumber = lineMappings.lineNumber(end.line); - return { - startLineNumber, - startColumn: start.character + 1, - endLineNumber, - endColumn: end.character + 1, - }; - }); - files.push({ - repo: repoUri, - file, - language, - uri: url, - revision, - code, - lineNumbers, - highlights, - }); - } - } - return { title, files: groupBy(files, 'repo'), uri, position }; + const title: string = await findTitleFromHover(hover, uri, position); + const files = await groupFiles(response.result, async loc => { + const ep = await codeServices.locate(req, loc.uri); + return await gitService.blob(ep, loc); + }); + return { title, files, uri, position }; } catch (error) { log.error(error); if (error instanceof ResponseError) { @@ -217,8 +192,7 @@ export function symbolByQnameRoute(router: CodeServerRouter, log: Logger) { async handler(req: RequestFacade) { try { const symbolSearchClient = new SymbolSearchClient(new EsClientWithRequest(req), log); - const res = await symbolSearchClient.findByQname(req.params.qname); - return res; + return await symbolSearchClient.findByQname(req.params.qname); } catch (error) { return Boom.internal(`Search Exception`); } diff --git a/x-pack/legacy/plugins/code/server/utils/lsp_utils.test.ts b/x-pack/legacy/plugins/code/server/utils/lsp_utils.test.ts new file mode 100644 index 00000000000000..ee25000643eddf --- /dev/null +++ b/x-pack/legacy/plugins/code/server/utils/lsp_utils.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Location } from 'vscode-languageserver-types'; +import { groupFiles } from './lsp_utils'; + +test('group files', async () => { + const range = { + start: { character: 0, line: 1 }, + end: { character: 0, line: 2 }, + }; + const url1 = 'https://github.com/elastic/code/blob/master/1'; + const url2 = 'https://github.com/elastic/code/blob/master/2'; + const url3 = 'https://github.com/elastic/code2/blob/master/1'; + const locs = [ + { + uri: url1, + range, + }, + { + uri: url1, + range: { + start: { character: 0, line: 2 }, + end: { character: 0, line: 3 }, + }, + }, + { + uri: url2, + range, + }, + { + uri: url3, + range, + }, + ] as Location[]; + const fakeSource = `1 + 2 + 3 + 4 + 5 + `; + + const fakeLoader = () => Promise.resolve({ content: fakeSource, lang: 'test' }); + // @ts-ignore + const result = await groupFiles(locs, fakeLoader); + const files = result['github.com/elastic/code']; + expect(files.length).toBe(2); + const file = files.find((f: any) => f.uri === url1); + expect(file).not.toBeUndefined(); + expect(file.lineNumbers).toStrictEqual(['1', '2', '3', '4', '5', '..']); + expect(result['github.com/elastic/code2'].length).toBe(1); +}); diff --git a/x-pack/legacy/plugins/code/server/utils/lsp_utils.ts b/x-pack/legacy/plugins/code/server/utils/lsp_utils.ts new file mode 100644 index 00000000000000..0bd6ea0ca05fc6 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/utils/lsp_utils.ts @@ -0,0 +1,122 @@ +/* + * 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 { groupBy, last } from 'lodash'; +import { Location, Position } from 'vscode-languageserver-types'; +import { ResponseMessage } from 'vscode-jsonrpc/lib/messages'; +import { CTAGS, GO } from '../lsp/language_servers'; +import { + expandRanges, + extractSourceContent, + LineMapping, + mergeRanges, +} from './composite_source_merger'; +import { detectLanguage } from './detect_language'; +import { parseLspUrl } from '../../common/uri_util'; +import { GitServiceDefinition } from '../distributed/apis'; + +type SourceLoader = ( + loc: typeof GitServiceDefinition.blob.request +) => Promise; + +export interface File { + repo: string; + file: string; + language: string; + uri: string; + revision: string; + code: string; + lineNumbers: number[]; + highlights: any[]; +} + +export interface GroupedFiles { + [repo: string]: File[]; +} + +export async function groupFiles( + list: Location[], + sourceLoader: SourceLoader +): Promise { + const files = []; + const groupedLocations = groupBy(list, 'uri'); + for (const url of Object.keys(groupedLocations)) { + const { repoUri, revision, file } = parseLspUrl(url)!; + const locations: Location[] = groupedLocations[url]; + const lines = locations.map(l => ({ + startLine: l.range.start.line, + endLine: l.range.end.line, + })); + const ranges = expandRanges(lines, 1); + const mergedRanges = mergeRanges(ranges); + try { + const blob = await sourceLoader({ uri: repoUri, path: file!, revision }); + if (blob.content) { + const source = blob.content.split('\n'); + const language = blob.lang; + const lineMappings = new LineMapping(); + const code = extractSourceContent(mergedRanges, source, lineMappings).join('\n'); + const lineNumbers = lineMappings.toStringArray(); + const highlights = locations.map(l => { + const { start, end } = l.range; + const startLineNumber = lineMappings.lineNumber(start.line); + const endLineNumber = lineMappings.lineNumber(end.line); + return { + startLineNumber, + startColumn: start.character + 1, + endLineNumber, + endColumn: end.character + 1, + }; + }); + files.push({ + repo: repoUri, + file, + language, + uri: url, + revision, + code, + lineNumbers, + highlights, + }); + } + } catch (e) { + // can't load this file, ignore this result + } + } + return (groupBy(files, 'repo') as unknown) as GroupedFiles; +} + +export async function findTitleFromHover(hover: ResponseMessage, uri: string, position: Position) { + let title: string; + if (hover.result && hover.result.contents) { + if (Array.isArray(hover.result.contents)) { + const content = hover.result.contents[0]; + title = hover.result.contents[0].value; + const lang = await detectLanguage(uri.replace('file://', '')); + // TODO(henrywong) Find a gernal approach to construct the reference title. + if (content.kind) { + // The format of the hover result is 'MarkupContent', extract appropriate pieces as the references title. + if (GO.languages.includes(lang)) { + title = title.substring(title.indexOf('```go\n') + 5, title.lastIndexOf('\n```')); + if (title.includes('{\n')) { + title = title.substring(0, title.indexOf('{\n')); + } + } + } else if (CTAGS.languages.includes(lang)) { + // There are language servers may provide hover results with markdown syntax, like ctags-langserver, + // extract the plain text. + if (title.substring(0, 2) === '**' && title.includes('**\n')) { + title = title.substring(title.indexOf('**\n') + 3); + } + } + } else { + title = hover.result.contents as 'string'; + } + } else { + title = last(uri.split('/')) + `(${position.line}, ${position.character})`; + } + return title; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap rename to x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/auto_follow_pattern_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/auto_follow_pattern_serialization.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/auto_follow_pattern_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/auto_follow_pattern_serialization.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js index 33bb7878e4eaaf..6a810febcba40c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js @@ -41,6 +41,8 @@ import { AutoFollowPatternIndicesPreview } from './auto_follow_pattern_indices_p import { RemoteClustersFormField } from './remote_clusters_form_field'; import { validateAutoFollowPattern, validateLeaderIndexPattern } from '../services/auto_follow_pattern_validators'; +import { AutoFollowPatternRequestFlyout } from './auto_follow_pattern_request_flyout'; + const indexPatternIllegalCharacters = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.join(' '); const indexNameIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); @@ -88,9 +90,16 @@ export class AutoFollowPatternForm extends PureComponent { fieldsErrors: validateAutoFollowPattern(autoFollowPattern), areErrorsVisible: false, isNew, + isRequestVisible: false, }; } + toggleRequest = () => { + this.setState(({ isRequestVisible }) => ({ + isRequestVisible: !isRequestVisible, + })); + }; + onFieldsChange = (fields) => { this.setState(({ autoFollowPattern }) => ({ autoFollowPattern: { @@ -590,7 +599,7 @@ export class AutoFollowPatternForm extends PureComponent { */ const renderActions = () => { const { apiStatus, saveButtonLabel } = this.props; - const { areErrorsVisible } = this.state; + const { areErrorsVisible, isRequestVisible } = this.state; if (apiStatus === API_STATUS.SAVING) { return ( @@ -614,30 +623,49 @@ export class AutoFollowPatternForm extends PureComponent { const isSaveDisabled = areErrorsVisible && !this.isFormValid(); return ( - - - - {saveButtonLabel} - - + + + + + {saveButtonLabel} + + + + + + + + - + {isRequestVisible ? ( + + ) : ( + + )} @@ -674,10 +702,25 @@ export class AutoFollowPatternForm extends PureComponent { } render() { + const { + autoFollowPattern, + isRequestVisible, + isNew, + } = this.state; + return ( {this.renderForm()} {this.renderLoading()} + + {isRequestVisible ? ( + this.setState({ isRequestVisible: false })} + /> + ) : null} ); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js new file mode 100644 index 00000000000000..68d58f68275d37 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js @@ -0,0 +1,108 @@ +/* + * 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. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { PureComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import PropTypes from 'prop-types'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { serializeAutoFollowPattern } from '../../../common/services/auto_follow_pattern_serialization'; + +export class AutoFollowPatternRequestFlyout extends PureComponent { + static propTypes = { + close: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + autoFollowPattern: PropTypes.object.isRequired, + isNew: PropTypes.bool, + }; + + render() { + const { name, autoFollowPattern, close, isNew } = this.props; + const endpoint = `PUT /_ccr/auto_follow/${name ? name : ''}`; + const payload = JSON.stringify(serializeAutoFollowPattern(autoFollowPattern), null, 2); + const request = `${endpoint}\n${payload}`; + + return ( + + + +

+ {name ? ( + + ) : ( + + )} +

+
+
+ + + +

+ {isNew ? ( + + ) : ( + + )} +

+
+ + + + + {request} + +
+ + + + + + +
+ ); + } +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js index f62e19eb937d7a..7fdc77e2a6e86c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js @@ -45,6 +45,8 @@ import { extractQueryParams } from '../../services/query_params'; import { getRemoteClusterName } from '../../services/get_remote_cluster_name'; import { RemoteClustersFormField } from '../remote_clusters_form_field'; +import { FollowerIndexRequestFlyout } from './follower_index_request_flyout'; + const indexNameIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); const fieldToValidatorMap = advancedSettingsFields.reduce((map, advancedSetting) => { @@ -121,12 +123,19 @@ export class FollowerIndexForm extends PureComponent { areErrorsVisible: false, areAdvancedSettingsVisible, isValidatingIndexName: false, + isRequestVisible: false, }; this.cachedAdvancedSettings = {}; this.validateIndexName = debounce(this.validateIndexName, 500); } + toggleRequest = () => { + this.setState(({ isRequestVisible }) => ({ + isRequestVisible: !isRequestVisible, + })); + }; + onFieldsChange = (fields) => { this.setState(updateFields(fields)); @@ -633,7 +642,7 @@ export class FollowerIndexForm extends PureComponent { */ const renderActions = () => { const { apiStatus, saveButtonLabel } = this.props; - const { areErrorsVisible } = this.state; + const { areErrorsVisible, isRequestVisible } = this.state; if (apiStatus === API_STATUS.SAVING) { return ( @@ -657,30 +666,50 @@ export class FollowerIndexForm extends PureComponent { const isSaveDisabled = areErrorsVisible && !this.isFormValid(); return ( - - - - {saveButtonLabel} - - + + + + + {saveButtonLabel} + + + + + + + + + - + {isRequestVisible ? ( + + ) : ( + + )} @@ -718,10 +747,23 @@ export class FollowerIndexForm extends PureComponent { } render() { + const { + followerIndex, + isRequestVisible, + } = this.state; + return ( {this.renderForm()} {this.renderLoading()} + + {isRequestVisible ? ( + this.setState({ isRequestVisible: false })} + /> + ) : null} ); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js new file mode 100644 index 00000000000000..c2b81d99171d6f --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js @@ -0,0 +1,92 @@ +/* + * 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. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { PureComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import PropTypes from 'prop-types'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; + +export class FollowerIndexRequestFlyout extends PureComponent { + static propTypes = { + close: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + followerIndex: PropTypes.object.isRequired, + }; + + render() { + const { name, followerIndex, close } = this.props; + const endpoint = `PUT /${name ? name : ''}/_ccr/follow`; + const payload = JSON.stringify(serializeFollowerIndex(followerIndex), null, 2); + const request = `${endpoint}\n${payload}`; + + return ( + + + +

+ +

+
+
+ + + +

+ +

+
+ + + + + {request} + +
+ + + + + + +
+ ); + } +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js index 3f8e149659ae6f..2e4419e3fd1cd0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js @@ -11,7 +11,7 @@ import { deserializeAutoFollowPattern, deserializeListAutoFollowPatterns, serializeAutoFollowPattern -} from '../../lib/auto_follow_pattern_serialization'; +} from '../../../common/services/auto_follow_pattern_serialization'; import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; import { API_BASE_PATH } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js index b8eddc04937d3d..77ab79b6d85a93 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { deserializeAutoFollowPattern } from '../../../common/services/auto_follow_pattern_serialization'; import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { registerAutoFollowPatternRoutes } from './auto_follow_pattern'; import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../fixtures'; -import { deserializeAutoFollowPattern } from '../../lib/auto_follow_pattern_serialization'; +import { registerAutoFollowPatternRoutes } from './auto_follow_pattern'; jest.mock('../../lib/call_with_request_factory'); jest.mock('../../lib/is_es_error_factory'); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js index 99143b0a4ed96c..af35215a5a770b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js @@ -6,12 +6,12 @@ import Boom from 'boom'; +import { API_BASE_PATH } from '../../../common/constants'; import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsErrorFactory } from '../../lib/is_es_error_factory'; import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization'; import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../common/constants'; export const registerCcrRoutes = (server) => { const isEsError = isEsErrorFactory(server); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js index 46e9c335a5f141..f1472c656ac4a8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js @@ -5,18 +5,19 @@ */ import Boom from 'boom'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; + import { deserializeFollowerIndex, deserializeListFollowerIndices, serializeFollowerIndex, serializeAdvancedSettings, -} from '../../lib/follower_index_serialization'; -import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; +} from '../../../common/services/follower_index_serialization'; import { API_BASE_PATH } from '../../../common/constants'; import { removeEmptyFields } from '../../../common/services/utils'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsErrorFactory } from '../../lib/is_es_error_factory'; +import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; +import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; export const registerFollowerIndexRoutes = (server) => { const isEsError = isEsErrorFactory(server); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js index 5d4a6c2a567952..25befb6958f6ae 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { registerFollowerIndexRoutes } from './follower_index'; +import { deserializeFollowerIndex } from '../../../common/services/follower_index_serialization'; import { getFollowerIndexStatsMock, getFollowerIndexListStatsMock, getFollowerIndexInfoMock, getFollowerIndexListInfoMock, } from '../../../fixtures'; -import { deserializeFollowerIndex } from '../../lib/follower_index_serialization'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsErrorFactory } from '../../lib/is_es_error_factory'; +import { registerFollowerIndexRoutes } from './follower_index'; jest.mock('../../lib/call_with_request_factory'); jest.mock('../../lib/is_es_error_factory'); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js index 8ce3d9f2552dee..50e012ed99a53b 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js @@ -9,11 +9,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import PropTypes from 'prop-types'; import { + EuiButtonEmpty, EuiCodeBlock, EuiFlyout, EuiFlyoutBody, + EuiFlyoutFooter, EuiFlyoutHeader, - EuiPortal, EuiSpacer, EuiText, EuiTitle, @@ -39,48 +40,59 @@ export class PolicyJsonFlyout extends PureComponent { const request = `${endpoint}\n${this.getEsJson(lifecycle)}`; return ( - - - - -

- {policyName ? ( - - ) : ( - - )} -

-
-
- - - -

+ + + +

+ {policyName ? ( + + ) : ( -

- + )} +

+ + + + + +

+ +

+
+ + - + + {request} + +
- - {request} - -
-
-
+ + + + + + ); } } diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js index a7f7cf02550dc9..5cf5c47ec6ac31 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js @@ -130,9 +130,11 @@ export class EditPolicy extends Component { showNodeDetailsFlyout = selectedNodeAttrsForDetails => { this.setState({ isShowingNodeDetailsFlyout: true, selectedNodeAttrsForDetails }); }; - showPolicyJsonFlyout = () => { - this.setState({ isShowingPolicyJsonFlyout: true }); + + togglePolicyJsonFlyout = () => { + this.setState(({ isShowingPolicyJsonFlyout }) => ({ isShowingPolicyJsonFlyout: !isShowingPolicyJsonFlyout })); }; + render() { const { selectedPolicy, @@ -145,7 +147,7 @@ export class EditPolicy extends Component { originalPolicyName, } = this.props; const selectedPolicyName = selectedPolicy.name; - const { isShowingErrors } = this.state; + const { isShowingErrors, isShowingPolicyJsonFlyout } = this.state; return ( @@ -167,6 +169,7 @@ export class EditPolicy extends Component { })} +
@@ -188,7 +191,9 @@ export class EditPolicy extends Component { />

+ + {isNewPolicy ? null : ( @@ -212,6 +217,7 @@ export class EditPolicy extends Component { + )} + {saveAsNewPolicy || isNewPolicy ? ( ) : null} + + + + + + + + + + @@ -322,6 +339,7 @@ export class EditPolicy extends Component { )} + + - - + + {isShowingPolicyJsonFlyout ? ( + + ) : ( + + )} + {this.state.isShowingNodeDetailsFlyout ? ( this.setState({ isShowingNodeDetailsFlyout: false })} /> ) : null} + {this.state.isShowingPolicyJsonFlyout ? ( ; export type InfraMetadataHost = rt.TypeOf; -export type InfraMEtadataOS = rt.TypeOf; +export type InfraMetadataOS = rt.TypeOf; + +export type InfraMetadataNodeType = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts index ff4490d2c700ae..82cfb0f83ed692 100644 --- a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts +++ b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts @@ -6,5 +6,8 @@ import { JobType } from './log_analysis'; +export const getJobIdPrefix = (spaceId: string, sourceId: string) => + `kibana-logs-ui-${spaceId}-${sourceId}-`; + export const getJobId = (spaceId: string, sourceId: string, jobType: JobType) => - `kibana-logs-ui-${spaceId}-${sourceId}-${jobType}`; + `${getJobIdPrefix(spaceId, sourceId)}${jobType}`; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx index b7947c77a2cc06..8b71fd405e0734 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx @@ -38,7 +38,7 @@ export const DensityChart: React.SFC = ({ const xMax = max(buckets.map(bucket => bucket.entriesCount)) || 0; const xScale = scaleLinear() .domain([0, xMax]) - .range([0, width / 2]); + .range([0, width * (2 / 3)]); const path = area() .x0(xScale(0)) @@ -47,24 +47,37 @@ export const DensityChart: React.SFC = ({ .curve(curveMonotoneY); const pathData = path(buckets); + const highestPathCoord = String(pathData) + .replace(/[^.0-9,]/g, ' ') + .split(/[ ,]/) + .reduce((result, num) => (Number(num) > result ? Number(num) : result), 0); return ( - + + + - ); }; -const PositiveAreaPath = euiStyled.path` - fill: ${props => - props.theme.darkMode - ? props.theme.eui.euiColorMediumShade - : props.theme.eui.euiColorLightShade}; +const DensityChartNegativeBackground = euiStyled.rect` + fill: white; `; -const NegativeAreaPath = euiStyled.path` +const DensityChartPositiveBackground = euiStyled.rect` fill: ${props => props.theme.darkMode ? props.theme.eui.euiColorLightShade : props.theme.eui.euiColorLightestShade}; `; + +const PositiveAreaPath = euiStyled.path` + fill: ${props => + props.theme.darkMode + ? props.theme.eui.euiColorMediumShade + : props.theme.eui.euiColorLightShade}; +`; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx index 98eb85c72628b4..08902f2b1644b1 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx @@ -14,6 +14,7 @@ interface HighlightedIntervalProps { start: number; end: number; width: number; + target: number | null; } export const HighlightedInterval: React.SFC = ({ @@ -22,20 +23,38 @@ export const HighlightedInterval: React.SFC = ({ getPositionOfTime, start, width, + target, }) => { const yStart = getPositionOfTime(start); const yEnd = getPositionOfTime(end); + const yTarget = target && getPositionOfTime(target); return ( - + <> + {yTarget && ( + + )} + + ); }; HighlightedInterval.displayName = 'HighlightedInterval'; +const HighlightTargetMarker = euiStyled.line` + stroke: ${props => props.theme.eui.euiColorPrimary}; + stroke-width: 1; +`; + const HighlightPolygon = euiStyled.polygon` fill: ${props => props.theme.eui.euiColorPrimary}; fill-opacity: 0.3; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx index f84fe6b713b899..9c81cda0a3fb6c 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx @@ -15,13 +15,20 @@ import { SearchMarkers } from './search_markers'; import { TimeRuler } from './time_ruler'; import { SummaryBucket, SummaryHighlightBucket } from './types'; +interface Interval { + end: number; + start: number; +} + +interface DragRecord { + startY: number; + currentY: number | null; +} + interface LogMinimapProps { className?: string; height: number; - highlightedInterval: { - end: number; - start: number; - } | null; + highlightedInterval: Interval | null; jumpToTarget: (params: LogEntryTime) => any; intervalSize: number; summaryBuckets: SummaryBucket[]; @@ -31,33 +38,114 @@ interface LogMinimapProps { } interface LogMinimapState { + target: number | null; + drag: DragRecord | null; + svgPosition: ClientRect; timeCursorY: number; } +function calculateYScale(target: number | null, height: number, intervalSize: number) { + const domainStart = target ? target - intervalSize / 2 : 0; + const domainEnd = target ? target + intervalSize / 2 : 0; + return scaleLinear() + .domain([domainStart, domainEnd]) + .range([0, height]); +} + export class LogMinimap extends React.Component { - public readonly state = { - timeCursorY: 0, - }; + constructor(props: LogMinimapProps) { + super(props); + this.state = { + timeCursorY: 0, + target: props.target, + drag: null, + svgPosition: { + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + }; + } - public handleClick: React.MouseEventHandler = event => { - const svgPosition = event.currentTarget.getBoundingClientRect(); + private dragTargetArea: SVGElement | null = null; + + public static getDerivedStateFromProps({ target }: LogMinimapProps, { drag }: LogMinimapState) { + if (!drag) { + return { target }; + } + return null; + } + + public handleClick = (event: MouseEvent) => { + const { svgPosition } = this.state; const clickedYPosition = event.clientY - svgPosition.top; const clickedTime = Math.floor(this.getYScale().invert(clickedYPosition)); - + this.setState({ + drag: null, + }); this.props.jumpToTarget({ tiebreaker: 0, time: clickedTime, }); }; - public getYScale = () => { - const { height, intervalSize, target } = this.props; + private handleMouseDown: React.MouseEventHandler = event => { + const { clientY, target } = event; + if (target === this.dragTargetArea) { + const svgPosition = event.currentTarget.getBoundingClientRect(); + this.setState({ + drag: { + startY: clientY, + currentY: null, + }, + svgPosition, + }); + window.addEventListener('mousemove', this.handleDragMove); + } + window.addEventListener('mouseup', this.handleMouseUp); + }; + + private handleMouseUp = (event: MouseEvent) => { + window.removeEventListener('mousemove', this.handleDragMove); + window.removeEventListener('mouseup', this.handleMouseUp); - const domainStart = target ? target - intervalSize / 2 : 0; - const domainEnd = target ? target + intervalSize / 2 : 0; - return scaleLinear() - .domain([domainStart, domainEnd]) - .range([0, height]); + const { drag, svgPosition } = this.state; + if (!drag || !drag.currentY) { + this.handleClick(event); + return; + } + const getTime = (pos: number) => Math.floor(this.getYScale().invert(pos)); + const startYPosition = drag.startY - svgPosition.top; + const endYPosition = event.clientY - svgPosition.top; + const startTime = getTime(startYPosition); + const endTime = getTime(endYPosition); + const timeDifference = endTime - startTime; + const newTime = (this.props.target || 0) - timeDifference; + this.setState({ drag: null, target: newTime }); + this.props.jumpToTarget({ + tiebreaker: 0, + time: newTime, + }); + }; + + private handleDragMove = (event: MouseEvent) => { + const { drag } = this.state; + if (!drag) return; + this.setState({ + drag: { + ...drag, + currentY: event.clientY, + }, + }); + }; + + public getYScale = () => { + const { target } = this.state; + const { height, intervalSize } = this.props; + return calculateYScale(target, height, intervalSize); }; public getPositionOfTime = (time: number) => { @@ -65,7 +153,7 @@ export class LogMinimap extends React.Component = event => { @@ -84,12 +172,20 @@ export class LogMinimap extends React.Component - - - - + + + + + + {highlightedInterval ? ( ) : null} @@ -128,14 +234,25 @@ export class LogMinimap extends React.Component - + + { + this.dragTargetArea = node; + }} + x={0} + y={0} + width={width / 3} + height={height} + /> ); } } -const MinimapBackground = euiStyled.rect` - fill: ${props => props.theme.eui.euiColorLightestShade}; +const DragTargetArea = euiStyled.rect<{ isGrabbing: boolean }>` + fill: transparent; + cursor: ${({ isGrabbing }) => (isGrabbing ? 'grabbing' : 'grab')}; `; const MinimapBorder = euiStyled.line` @@ -152,7 +269,9 @@ const TimeCursor = euiStyled.line` : props.theme.eui.euiColorDarkShade}; `; -const MinimapWrapper = euiStyled.svg` +const MinimapWrapper = euiStyled.svg<{ showOverscanBoundaries: boolean }>` + background: ${props => + props.showOverscanBoundaries ? props.theme.eui.euiColorMediumShade : 'transparent'}; & ${TimeCursor} { visibility: hidden; } diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx index 16bb6dfd1fe85a..eb49c8d010a90f 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx @@ -25,16 +25,35 @@ export const TimeRuler: React.SFC = ({ end, height, start, tickC const ticks = yScale.ticks(tickCount); const formatTick = yScale.tickFormat(); + const dateModLabel = (() => { + for (let i = 0; i < ticks.length; i++) { + const tickLabel = formatTick(ticks[i]); + if (!tickLabel[0].match(/[0-9]/)) { + return i % 12; + } + } + })(); + return ( {ticks.map((tick, tickIndex) => { const y = yScale(tick); + const isLabeledTick = tickIndex % 12 === dateModLabel; + const tickStartX = isLabeledTick ? 0 : width / 3 - 4; return ( - - {formatTick(tick)} - - + {isLabeledTick && ( + + {formatTick(tick)} + + )} + ); })} @@ -45,16 +64,22 @@ export const TimeRuler: React.SFC = ({ end, height, start, tickC TimeRuler.displayName = 'TimeRuler'; const TimeRulerTickLabel = euiStyled.text` - font-size: ${props => props.theme.eui.euiFontSizeXS}; + font-size: 9px; line-height: ${props => props.theme.eui.euiLineHeight}; fill: ${props => props.theme.eui.textColors.subdued}; + user-select: none; pointer-events: none; `; -const TimeRulerGridLine = euiStyled.line` +const TimeRulerGridLine = euiStyled.line<{ isDark: boolean }>` stroke: ${props => - props.theme.darkMode ? props.theme.eui.euiColorDarkShade : props.theme.eui.euiColorMediumShade}; - stroke-dasharray: 2, 2; + props.isDark + ? props.theme.darkMode + ? props.theme.eui.euiColorDarkestShade + : props.theme.eui.euiColorDarkShade + : props.theme.darkMode + ? props.theme.eui.euiColorDarkShade + : props.theme.eui.euiColorMediumShade}; stroke-opacity: 0.5; stroke-width: 1px; `; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index b9a4d4244a1ff6..40a5d8c30d4bb8 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -44,6 +44,7 @@ interface ScrollableLogTextStreamViewProps { startKey: TimeKey | null; middleKey: TimeKey | null; endKey: TimeKey | null; + fromScroll: boolean; }) => any; loadNewerItems: () => void; setFlyoutItem: (id: string) => void; @@ -253,12 +254,14 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< bottomChild, pagesAbove, pagesBelow, + fromScroll, }: { topChild: string; middleChild: string; bottomChild: string; pagesAbove: number; pagesBelow: number; + fromScroll: boolean; }) => { this.props.reportVisibleInterval({ endKey: parseStreamItemId(bottomChild), @@ -266,6 +269,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< pagesAfterEnd: pagesBelow, pagesBeforeStart: pagesAbove, startKey: parseStreamItemId(topChild), + fromScroll, }); } ); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx index 5b57bd14d181b1..8700239d84f62f 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx @@ -22,6 +22,7 @@ interface VerticalScrollPanelProps { bottomChild: Child; pagesAbove: number; pagesBelow: number; + fromScroll: boolean; }) => void; target: Child | undefined; height: number; @@ -52,11 +53,17 @@ export class VerticalScrollPanel extends React.PureComponent< public scrollRef = React.createRef(); public childRefs = new Map(); public childDimensions = new Map(); + private nextScrollEventFromCenterTarget = false; public handleScroll: React.UIEventHandler = throttle( SCROLL_THROTTLE_INTERVAL, () => { - this.reportVisibleChildren(); + // If this event was fired by the centerTarget method modifying the scrollTop, + // then don't send `fromScroll: true` to reportVisibleChildren. The rest of the + // app needs to respond differently depending on whether the user is scrolling through + // the pane manually, versus whether the pane is updating itself in response to new data + this.reportVisibleChildren(!this.nextScrollEventFromCenterTarget); + this.nextScrollEventFromCenterTarget = false; } ); @@ -121,7 +128,7 @@ export class VerticalScrollPanel extends React.PureComponent< }; }; - public reportVisibleChildren = () => { + public reportVisibleChildren = (fromScroll: boolean = false) => { const { onVisibleChildrenChange } = this.props; const visibleChildren = this.getVisibleChildren(); const scrollPosition = this.getScrollPosition(); @@ -134,6 +141,7 @@ export class VerticalScrollPanel extends React.PureComponent< bottomChild: visibleChildren.bottomChild, middleChild: visibleChildren.middleChild, topChild: visibleChildren.topChild, + fromScroll, ...scrollPosition, }); }; @@ -153,6 +161,9 @@ export class VerticalScrollPanel extends React.PureComponent< if (targetDimensions) { const targetOffset = typeof offset === 'undefined' ? targetDimensions.height / 2 : offset; + // Flag the scrollTop change that's about to happen as programmatic, as + // opposed to being in direct response to user input + this.nextScrollEventFromCenterTarget = true; scrollRef.current.scrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2; } }; diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/aggregation.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/aggregation.tsx index 72e0342f8d7ca5..a41eb742d40a7c 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/aggregation.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/aggregation.tsx @@ -45,7 +45,7 @@ export const MetricsExplorerAggregationPicker = injectI18n(({ intl, options, onC }), [MetricsExplorerAggregation.count]: intl.formatMessage({ id: 'xpack.infra.metricsExplorer.aggregationLables.count', - defaultMessage: 'Document Count', + defaultMessage: 'Document count', }), }; diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_options.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_options.tsx index b44ba147784482..a6b5384795392b 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_options.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_options.tsx @@ -51,14 +51,14 @@ export const MetricsExplorerChartOptions = injectI18n(({ chartOptions, onChange, id: MetricsExplorerYAxisMode.auto, label: intl.formatMessage({ id: 'xpack.infra.metricsExplorer.chartOptions.autoLabel', - defaultMessage: 'Automatic (Min to Max)', + defaultMessage: 'Automatic (min to max)', }), }, { id: MetricsExplorerYAxisMode.fromZero, label: intl.formatMessage({ id: 'xpack.infra.metricsExplorer.chartOptions.fromZeroLabel', - defaultMessage: 'From Zero (0 to Max)', + defaultMessage: 'From zero (0 to max)', }), }, ]; @@ -122,7 +122,7 @@ export const MetricsExplorerChartOptions = injectI18n(({ chartOptions, onChange, compressed label={intl.formatMessage({ id: 'xpack.infra.metricsExplorer.chartOptions.typeLabel', - defaultMessage: 'Chart Style', + defaultMessage: 'Chart style', })} > } diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index ee0e605baaf5cc..57863d8d2df82b 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -46,7 +46,7 @@ export const IndicesConfigurationPanel = ({ title={ } description={ @@ -92,7 +92,7 @@ export const IndicesConfigurationPanel = ({ title={ } description={ diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts b/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts index 9f74b6e25cad77..7e5a51742802d3 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts +++ b/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts @@ -26,11 +26,11 @@ export const fieldToName = (field: string, intl: InjectedIntl) => { }), 'cloud.availability_zone': intl.formatMessage({ id: 'xpack.infra.groupByDisplayNames.availabilityZone', - defaultMessage: 'Availability Zone', + defaultMessage: 'Availability zone', }), 'cloud.machine.type': intl.formatMessage({ id: 'xpack.infra.groupByDisplayNames.machineType', - defaultMessage: 'Machine Type', + defaultMessage: 'Machine type', }), 'cloud.project.id': intl.formatMessage({ id: 'xpack.infra.groupByDisplayNames.projectID', @@ -38,11 +38,11 @@ export const fieldToName = (field: string, intl: InjectedIntl) => { }), 'cloud.provider': intl.formatMessage({ id: 'xpack.infra.groupByDisplayNames.provider', - defaultMessage: 'Cloud Provider', + defaultMessage: 'Cloud provider', }), 'service.type': intl.formatMessage({ id: 'xpack.infra.groupByDisplayNames.serviceType', - defaultMessage: 'Service Type', + defaultMessage: 'Service type', }), }; return LOOKUP[field] || field; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/view_switcher.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/view_switcher.tsx index 03136e99a9dac1..ac18120d9ad302 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/view_switcher.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/view_switcher.tsx @@ -20,7 +20,7 @@ export const ViewSwitcher = injectI18n(({ view, onChange, intl }: Props) => { id: 'map', label: intl.formatMessage({ id: 'xpack.infra.viewSwitcher.mapViewLabel', - defaultMessage: 'Map View', + defaultMessage: 'Map view', }), iconType: 'apps', }, @@ -28,7 +28,7 @@ export const ViewSwitcher = injectI18n(({ view, onChange, intl }: Props) => { id: 'table', label: intl.formatMessage({ id: 'xpack.infra.viewSwitcher.tableViewLabel', - defaultMessage: 'Table View', + defaultMessage: 'Table view', }), iconType: 'editorUnorderedList', }, diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx index e39099899c416b..9f84ef9407a566 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx @@ -112,7 +112,7 @@ export const WaffleGroupByControls = injectI18n( { name: intl.formatMessage({ id: 'xpack.infra.waffle.customGroupByOptionName', - defaultMessage: 'Custom Field', + defaultMessage: 'Custom field', }), icon: 'empty', panel: 'customPanel', diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx index 9d49963e18a773..c74260d9819435 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx @@ -35,22 +35,32 @@ const getOptions = ( if (!OPTIONS) { const CPUUsage = intl.formatMessage({ id: 'xpack.infra.waffle.metricOptions.cpuUsageText', - defaultMessage: 'CPU Usage', + defaultMessage: 'CPU usage', }); const MemoryUsage = intl.formatMessage({ id: 'xpack.infra.waffle.metricOptions.memoryUsageText', - defaultMessage: 'Memory Usage', + defaultMessage: 'Memory usage', }); const InboundTraffic = intl.formatMessage({ id: 'xpack.infra.waffle.metricOptions.inboundTrafficText', - defaultMessage: 'Inbound Traffic', + defaultMessage: 'Inbound traffic', }); const OutboundTraffic = intl.formatMessage({ id: 'xpack.infra.waffle.metricOptions.outboundTrafficText', - defaultMessage: 'Outbound Traffic', + defaultMessage: 'Outbound traffic', + }); + + const LogRate = intl.formatMessage({ + id: 'xpack.infra.waffle.metricOptions.hostLogRateText', + defaultMessage: 'Log rate', + }); + + const Load = intl.formatMessage({ + id: 'xpack.infra.waffle.metricOptions.loadText', + defaultMessage: 'Load', }); OPTIONS = { @@ -100,10 +110,7 @@ const getOptions = ( value: InfraSnapshotMetricType.memory, }, { - text: intl.formatMessage({ - id: 'xpack.infra.waffle.metricOptions.loadText', - defaultMessage: 'Load', - }), + text: Load, value: InfraSnapshotMetricType.load, }, { @@ -115,10 +122,7 @@ const getOptions = ( value: InfraSnapshotMetricType.tx, }, { - text: intl.formatMessage({ - id: 'xpack.infra.waffle.metricOptions.hostLogRateText', - defaultMessage: 'Log Rate', - }), + text: LogRate, value: InfraSnapshotMetricType.logRate, }, ], diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts new file mode 100644 index 00000000000000..e364e534a2d613 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { kfetch } from 'ui/kfetch'; +import { getJobId } from '../../../../../common/log_analysis'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; + +export const callJobsSummaryAPI = async (spaceId: string, sourceId: string) => { + const response = await kfetch({ + method: 'POST', + pathname: '/api/ml/jobs/jobs_summary', + body: JSON.stringify( + fetchJobStatusRequestPayloadRT.encode({ + jobIds: [getJobId(spaceId, sourceId, 'log-entry-rate')], + }) + ), + }); + return fetchJobStatusResponsePayloadRT.decode(response).getOrElseL(throwErrors(createPlainError)); +}; + +export const fetchJobStatusRequestPayloadRT = rt.type({ + jobIds: rt.array(rt.string), +}); + +export type FetchJobStatusRequestPayload = rt.TypeOf; + +// TODO: Get this to align with the payload - something is tripping it up somewhere +// export const fetchJobStatusResponsePayloadRT = rt.array(rt.type({ +// datafeedId: rt.string, +// datafeedIndices: rt.array(rt.string), +// datafeedState: rt.string, +// description: rt.string, +// earliestTimestampMs: rt.number, +// groups: rt.array(rt.string), +// hasDatafeed: rt.boolean, +// id: rt.string, +// isSingleMetricViewerJob: rt.boolean, +// jobState: rt.string, +// latestResultsTimestampMs: rt.number, +// latestTimestampMs: rt.number, +// memory_status: rt.string, +// nodeName: rt.union([rt.string, rt.undefined]), +// processed_record_count: rt.number, +// fullJob: rt.any, +// auditMessage: rt.any, +// deleting: rt.union([rt.boolean, rt.undefined]), +// })); + +export const fetchJobStatusResponsePayloadRT = rt.any; + +export type FetchJobStatusResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts index 784c02f89615ff..294a78cc852063 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './log_analysis_capabilities'; +export * from './log_analysis_jobs'; export * from './log_analysis_results'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx new file mode 100644 index 00000000000000..f364e37462f394 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo, useState, useEffect } from 'react'; +import { kfetch } from 'ui/kfetch'; + +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { + getMlCapabilitiesResponsePayloadRT, + GetMlCapabilitiesResponsePayload, +} from './ml_api_types'; +import { throwErrors, createPlainError } from '../../../../common/runtime_types'; + +export const useLogAnalysisCapabilities = () => { + const [mlCapabilities, setMlCapabilities] = useState( + initialMlCapabilities + ); + + const [fetchMlCapabilitiesRequest, fetchMlCapabilities] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + const rawResponse = await kfetch({ + method: 'GET', + pathname: '/api/ml/ml_capabilities', + }); + + return getMlCapabilitiesResponsePayloadRT + .decode(rawResponse) + .getOrElseL(throwErrors(createPlainError)); + }, + onResolve: response => { + setMlCapabilities(response); + }, + }, + [] + ); + + useEffect(() => { + fetchMlCapabilities(); + }, []); + + const isLoading = useMemo(() => fetchMlCapabilitiesRequest.state === 'pending', [ + fetchMlCapabilitiesRequest.state, + ]); + + return { + hasLogAnalysisCapabilites: mlCapabilities.capabilities.canCreateJob, + isLoading, + }; +}; + +const initialMlCapabilities = { + capabilities: { + canGetJobs: false, + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canForecastJob: false, + canGetDatafeeds: false, + canStartStopDatafeed: false, + canUpdateJob: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetCalendars: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canGetFilters: false, + canCreateFilter: false, + canDeleteFilter: false, + canFindFileStructure: false, + canGetDataFrameJobs: false, + canDeleteDataFrameJob: false, + canPreviewDataFrameJob: false, + canCreateDataFrameJob: false, + canStartStopDataFrameJob: false, + }, + isPlatinumOrTrialLicense: false, + mlFeatureEnabledInSpace: false, + upgradeInProgress: false, +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx new file mode 100644 index 00000000000000..391a4b190edd24 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate-latest'; +import { useMemo, useEffect, useState } from 'react'; +import { values } from 'lodash'; +import { getJobId } from '../../../../common/log_analysis'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api'; + +type JobStatus = 'unknown' | 'closed' | 'closing' | 'failed' | 'opened' | 'opening' | 'deleted'; +// type DatafeedStatus = 'unknown' | 'started' | 'starting' | 'stopped' | 'stopping' | 'deleted'; + +export const useLogAnalysisJobs = ({ + indexPattern, + sourceId, + spaceId, +}: { + indexPattern: string; + sourceId: string; + spaceId: string; +}) => { + const [jobStatus, setJobStatus] = useState<{ + logEntryRate: JobStatus; + }>({ + logEntryRate: 'unknown', + }); + + // const [setupMlModuleRequest, setupMlModule] = useTrackedPromise( + // { + // cancelPreviousOn: 'resolution', + // createPromise: async () => { + // kfetch({ + // method: 'POST', + // pathname: '/api/ml/modules/setup', + // body: JSON.stringify( + // setupMlModuleRequestPayloadRT.encode({ + // indexPatternName: indexPattern, + // prefix: getJobIdPrefix(spaceId, sourceId), + // startDatafeed: true, + // }) + // ), + // }); + // }, + // }, + // [indexPattern, spaceId, sourceId] + // ); + + const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return callJobsSummaryAPI(spaceId, sourceId); + }, + onResolve: response => { + if (response && response.length) { + const logEntryRate = response.find( + (job: any) => job.id === getJobId(spaceId, sourceId, 'log-entry-rate') + ); + setJobStatus({ + logEntryRate: logEntryRate ? logEntryRate.jobState : 'unknown', + }); + } + }, + onReject: error => { + // TODO: Handle errors + }, + }, + [indexPattern, spaceId, sourceId] + ); + + useEffect(() => { + fetchJobStatus(); + }, []); + + const isSetupRequired = useMemo(() => { + const jobStates = values(jobStatus); + return ( + jobStates.filter(state => state === 'opened' || state === 'opening').length < jobStates.length + ); + }, [jobStatus]); + + const isLoadingSetupStatus = useMemo(() => fetchJobStatusRequest.state === 'pending', [ + fetchJobStatusRequest.state, + ]); + + return { + jobStatus, + isSetupRequired, + isLoadingSetupStatus, + }; +}; + +export const LogAnalysisJobs = createContainer(useLogAnalysisJobs); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx index 19034410378227..861c07c3ad5fa9 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import createContainer from 'constate-latest/dist/ts/src'; +import createContainer from 'constate-latest'; import { useMemo } from 'react'; import { useLogEntryRate } from './log_entry_rate'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts new file mode 100644 index 00000000000000..ee70edc31d49ba --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const getMlCapabilitiesResponsePayloadRT = rt.type({ + capabilities: rt.type({ + canGetJobs: rt.boolean, + canCreateJob: rt.boolean, + canDeleteJob: rt.boolean, + canOpenJob: rt.boolean, + canCloseJob: rt.boolean, + canForecastJob: rt.boolean, + canGetDatafeeds: rt.boolean, + canStartStopDatafeed: rt.boolean, + canUpdateJob: rt.boolean, + canUpdateDatafeed: rt.boolean, + canPreviewDatafeed: rt.boolean, + }), + isPlatinumOrTrialLicense: rt.boolean, + mlFeatureEnabledInSpace: rt.boolean, + upgradeInProgress: rt.boolean, +}); + +export type GetMlCapabilitiesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/index.ts new file mode 100644 index 00000000000000..224217e860e94d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/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 './page'; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx new file mode 100644 index 00000000000000..ef4a1ffe6735c8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.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 React, { useContext } from 'react'; +import chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; +import { ColumnarPage } from '../../../components/page'; +import { LoadingPage } from '../../../components/loading_page'; +import { AnalysisPageProviders } from './page_providers'; +import { AnalysisResultsContent } from './page_results_content'; +import { AnalysisSetupContent } from './page_setup_content'; +import { useLogAnalysisJobs } from '../../../containers/logs/log_analysis/log_analysis_jobs'; +import { Source } from '../../../containers/source'; + +export const AnalysisPage = () => { + const { sourceId, source } = useContext(Source.Context); + const spaceId = chrome.getInjected('activeSpace').space.id; + const { isSetupRequired, isLoadingSetupStatus } = useLogAnalysisJobs({ + indexPattern: source ? source.configuration.logAlias : '', + sourceId, + spaceId, + }); + + return ( + + + {isLoadingSetupStatus ? ( + + ) : isSetupRequired ? ( + + ) : ( + + )} + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx new file mode 100644 index 00000000000000..95d62781643267 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Source, useSource } from '../../../containers/source'; +import { useSourceId } from '../../../containers/source_id'; + +export const AnalysisPageProviders: React.FunctionComponent = ({ children }) => { + const [sourceId] = useSourceId(); + const source = useSource({ sourceId }); + + return {children}; +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx new file mode 100644 index 00000000000000..a570f9e37309f3 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { useTrackPageview } from '../../../hooks/use_track_metric'; + +export const AnalysisResultsContent = () => { + useTrackPageview({ app: 'infra_logs', path: 'analysis_results' }); + useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 }); + + return
Results
; +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx new file mode 100644 index 00000000000000..a4e1402e9ce605 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { useTrackPageview } from '../../../hooks/use_track_metric'; + +export const AnalysisSetupContent = () => { + useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); + useTrackPageview({ app: 'infra_logs', path: 'analysis_setup', delay: 15000 }); + + return
Setup
; +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx index dbcd046549c964..f2fcabe60e913a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -16,10 +16,15 @@ import { HelpCenterContent } from '../../components/help_center_content'; import { Header } from '../../components/header'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; import { ColumnarPage } from '../../components/page'; -import { Source } from '../../containers/source'; +import { SourceLoadingPage } from '../../components/source_loading_page'; +import { SourceErrorPage } from '../../components/source_error_page'; +import { Source, useSource } from '../../containers/source'; import { StreamPage } from './stream'; import { SettingsPage } from '../shared/settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; +import { AnalysisPage } from './analysis'; +import { useLogAnalysisCapabilities } from '../../containers/logs/log_analysis'; +import { useSourceId } from '../../containers/source_id'; interface LogsPageProps extends RouteComponentProps { intl: InjectedIntl; @@ -27,61 +32,90 @@ interface LogsPageProps extends RouteComponentProps { } export const LogsPage = injectUICapabilities( - injectI18n(({ match, intl, uiCapabilities }: LogsPageProps) => ( - - - - - + injectI18n(({ match, intl, uiCapabilities }: LogsPageProps) => { + const [sourceId] = useSourceId(); + const source = useSource({ sourceId }); + const { hasLogAnalysisCapabilites } = useLogAnalysisCapabilities(); + const streamTab = { + title: intl.formatMessage({ + id: 'xpack.infra.logs.index.streamTabTitle', + defaultMessage: 'Stream', + }), + path: `${match.path}/stream`, + }; + const analysisTab = { + title: intl.formatMessage({ + id: 'xpack.infra.logs.index.analysisTabTitle', + defaultMessage: 'Analysis', + }), + path: `${match.path}/analysis`, + }; + const settingsTab = { + title: intl.formatMessage({ + id: 'xpack.infra.logs.index.settingsTabTitle', + defaultMessage: 'Settings', + }), + path: `${match.path}/settings`, + }; + return ( + + + -
+ - - - + {source.isLoadingSource || + (!source.isLoadingSource && + !source.hasFailedLoadingSource && + source.source === undefined) ? ( + + ) : source.hasFailedLoadingSource ? ( + + ) : ( + <> + + + - - - - - - - )) + + + + + + + )} + + + ); + }) ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx index abcc881dd250ee..3188b3e03b0152 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -6,32 +6,12 @@ import React, { useContext } from 'react'; -import { SourceErrorPage } from '../../../components/source_error_page'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; import { Source } from '../../../containers/source'; import { LogsPageLogsContent } from './page_logs_content'; import { LogsPageNoIndicesContent } from './page_no_indices_content'; export const StreamPageContent: React.FunctionComponent = () => { - const { - hasFailedLoadingSource, - isLoadingSource, - logIndicesExist, - loadSource, - loadSourceFailureMessage, - } = useContext(Source.Context); + const { logIndicesExist } = useContext(Source.Context); - return ( - <> - {isLoadingSource ? ( - - ) : logIndicesExist ? ( - - ) : hasFailedLoadingSource ? ( - - ) : ( - - )} - - ); + return <>{logIndicesExist ? : }; }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index 00c52eecdbe346..caae36aac8a65a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -121,19 +121,30 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { {({ buckets }) => ( - {({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => ( - 0 ? logSummaryHighlights[0].buckets : [] - } - target={visibleMidpointTime} - /> + {({ + isAutoReloading, + jumpToTargetPosition, + visibleMidpointTime, + visibleTimeInterval, + }) => ( + + {({ isReloading }) => ( + 0 + ? logSummaryHighlights[0].buckets + : [] + } + target={visibleMidpointTime} + /> + )} + )} )} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx index b93cf48bde5e7e..55ffc807f7ba8b 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -4,27 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext } from 'react'; import { LogFlyout } from '../../../containers/logs/log_flyout'; import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights'; -import { Source, useSource } from '../../../containers/source'; -import { useSourceId } from '../../../containers/source_id'; +import { Source } from '../../../containers/source'; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - const [sourceId] = useSourceId(); - const source = useSource({ sourceId }); + const { sourceId, version } = useContext(Source.Context); return ( - - - - - {children} - - - - + + + + {children} + + + ); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index 46cf3aab40e9f5..8b7310e43dee52 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -126,6 +126,7 @@ export const LogsToolbar = injectI18n(({ intl }) => { jumpToTargetPositionTime, startLiveStreaming, stopLiveStreaming, + targetPosition, }) => ( ( diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts b/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts index 82162276a5c2db..878ecae686e405 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts @@ -36,6 +36,7 @@ export interface LogPositionState { middleKey: TimeKey | null; endKey: TimeKey | null; }; + controlsShouldDisplayTargetPosition: boolean; } export const initialLogPositionState: LogPositionState = { @@ -48,6 +49,7 @@ export const initialLogPositionState: LogPositionState = { middleKey: null, startKey: null, }, + controlsShouldDisplayTargetPosition: false, }; const targetPositionReducer = reducerWithInitialState(initialLogPositionState.targetPosition).case( @@ -74,8 +76,23 @@ const visiblePositionReducer = reducerWithInitialState( startKey, })); +// Determines whether to use the target position or the visible midpoint when +// displaying a timestamp or time range in the toolbar and log minimap. When the +// user jumps to a new target, the final visible midpoint is indeterminate until +// all the new data has finished loading, so using this flag reduces the perception +// that the UI is jumping around inaccurately +const controlsShouldDisplayTargetPositionReducer = reducerWithInitialState( + initialLogPositionState.controlsShouldDisplayTargetPosition +) + .case(jumpToTargetPosition, () => true) + .case(reportVisiblePositions, (state, { fromScroll }) => { + if (fromScroll) return false; + return state; + }); + export const logPositionReducer = combineReducers({ targetPosition: targetPositionReducer, updatePolicy: targetPositionUpdatePolicyReducer, visiblePositions: visiblePositionReducer, + controlsShouldDisplayTargetPosition: controlsShouldDisplayTargetPositionReducer, }); diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts index cb718ae33b8439..7104883a585c11 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts @@ -22,11 +22,17 @@ export const selectMiddleVisiblePosition = (state: LogPositionState) => export const selectLastVisiblePosition = (state: LogPositionState) => state.visiblePositions.endKey ? state.visiblePositions.endKey : null; +export const selectControlsShouldDisplayTargetPosition = (state: LogPositionState) => + state.controlsShouldDisplayTargetPosition; + export const selectVisibleMidpointOrTarget = createSelector( selectMiddleVisiblePosition, selectTargetPosition, - (middleVisiblePosition, targetPosition) => { - if (middleVisiblePosition) { + selectControlsShouldDisplayTargetPosition, + (middleVisiblePosition, targetPosition, displayTargetPosition) => { + if (displayTargetPosition) { + return targetPosition; + } else if (middleVisiblePosition) { return middleVisiblePosition; } else if (targetPosition) { return targetPosition; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index f663bb83c3293f..30277a7abb5e79 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -9,6 +9,7 @@ import { GraphQLSchema } from 'graphql'; import { Lifecycle, ResponseToolkit, RouteOptions } from 'hapi'; import { Legacy } from 'kibana'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { JsonObject } from '../../../../common/typed_json'; import { InfraMetricModel } from '../metrics/adapter_types'; @@ -66,6 +67,7 @@ export interface InfraBackendFrameworkAdapter { timerange: { min: number; max: number }, filters: JsonObject[] ): Promise; + config(req: InfraFrameworkRequest): KibanaConfig; } /* eslint-enable @typescript-eslint/unified-signatures */ diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 400bc9c18ebf36..ed5751a438a905 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -8,6 +8,7 @@ import { GenericParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; import { Legacy } from 'kibana'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { InfraMetricModel } from '../metrics/adapter_types'; import { InfraBackendFrameworkAdapter, @@ -36,6 +37,11 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework this.version = server.config().get('pkg.version'); } + public config(req: InfraFrameworkRequest): KibanaConfig { + const internalRequest = req[internalInfraFrameworkRequest]; + return internalRequest.server.config(); + } + public exposeStaticDir(urlPath: string, dir: string): void { this.server.route({ handler: { diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts index 34b3448b860746..14ab0513c4a64d 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts @@ -16,6 +16,7 @@ import { import { InfraBackendLibs } from '../../lib/infra_types'; import { getMetricMetadata } from './lib/get_metric_metadata'; import { pickFeatureName } from './lib/pick_feature_name'; +import { hasAPMData } from './lib/has_apm_data'; import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata'; import { getNodeInfo } from './lib/get_node_info'; import { throwErrors } from '../../../common/runtime_types'; @@ -54,12 +55,15 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { nameToFeature('metrics') ); + const hasAPM = await hasAPMData(framework, req, configuration, nodeId, nodeType); + const apmMetricFeatures = hasAPM ? [{ name: 'apm.transaction', source: 'apm' }] : []; + const id = metricsMetadata.id; const name = metricsMetadata.name || id; return InfraMetadataRT.decode({ id, name, - features: [...metricFeatures, ...cloudMetricsFeatures], + features: [...metricFeatures, ...cloudMetricsFeatures, ...apmMetricFeatures], info, }).getOrElseL(throwErrors(Boom.badImplementation)); } catch (error) { diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts new file mode 100644 index 00000000000000..3193cf83978b0b --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + InfraFrameworkRequest, + InfraBackendFrameworkAdapter, +} from '../../../lib/adapters/framework'; +import { InfraSourceConfiguration } from '../../../lib/sources'; +import { getIdFieldName } from './get_id_field_name'; + +export const hasAPMData = async ( + framework: InfraBackendFrameworkAdapter, + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeId: string, + nodeType: 'host' | 'pod' | 'container' +) => { + const config = framework.config(req); + const apmIndex = config.get('apm_oss.transactionIndices') || 'apm-*'; + // There is a bug in APM ECS data where host.name is not set. + // This will fixed with: https://github.com/elastic/apm-server/issues/2502 + const nodeFieldName = + nodeType === 'host' ? 'host.hostname' : getIdFieldName(sourceConfiguration, nodeType); + const params = { + allowNoIndices: true, + ignoreUnavailable: true, + terminateAfter: 1, + index: apmIndex, + body: { + size: 0, + query: { + bool: { + filter: [ + { + match: { [nodeFieldName]: nodeId }, + }, + { + exists: { field: 'service.name' }, + }, + { + exists: { field: 'transaction.type' }, + }, + ], + }, + }, + }, + }; + const response = await framework.callWithRequest<{}, {}>(req, 'search', params); + return response.hits.total.value !== 0; +}; diff --git a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js b/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js index d7e3694f9ebb29..cf156b9a791699 100755 --- a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js +++ b/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js @@ -6,7 +6,7 @@ import React from 'react'; import { toastNotifications } from 'ui/notify'; -import { MarkdownSimple } from 'ui/markdown'; +import { MarkdownSimple } from '../../../../../../../src/legacy/core_plugins/kibana_react/public'; import { PLUGIN } from '../../../common/constants'; export class LogstashLicenseService { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js index a48760a631c9bd..7a353a50dd675a 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js @@ -30,6 +30,7 @@ export class FeatureProperties extends React.Component { componentDidUpdate() { this._loadProperties(); + this.props.reevaluateTooltipPosition(); } componentWillUnmount() { @@ -109,7 +110,6 @@ export class FeatureProperties extends React.Component { } render() { - if (this.state.loadPropertiesErrorMsg) { return ( {}, - showFilterButtons: false + showFilterButtons: false, + reevaluateTooltipPosition: () => {}, }; const mockTooltipProperties = [ diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js index fd03fb288ece70..0e16a754f0332b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js @@ -125,6 +125,7 @@ export class FeatureTooltip extends React.Component { showFilterButtons={this.props.showFilterButtons} onCloseTooltip={this._onCloseTooltip} addFilters={this.props.addFilters} + reevaluateTooltipPosition={this.props.reevaluateTooltipPosition} /> ); } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index a583692101f2d5..e194508b79d0cf 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -454,6 +454,15 @@ export class MBMapContainer extends React.Component { addSpritesheetToMap(json, sprites, this._mbMap); } + _reevaluateTooltipPosition = () => { + // Force mapbox to ensure tooltip does not clip map boundary and move anchor when clipping occurs + requestAnimationFrame(() => { + if (this._isMounted && this.props.tooltipState && this.props.tooltipState.location) { + this._mbPopup.setLngLat(this.props.tooltipState.location); + } + }); + } + _hideTooltip() { if (this._mbPopup.isOpen()) { this._mbPopup.remove(); @@ -475,6 +484,7 @@ export class MBMapContainer extends React.Component { showFilterButtons={!!this.props.addFilters && isLocked} isLocked={isLocked} addFilters={this.props.addFilters} + reevaluateTooltipPosition={this._reevaluateTooltipPosition} /> ), this._tooltipContainer); diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap index 1aeee77882c41a..41316437e86731 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap @@ -3,67 +3,67 @@ exports[`Data Frame: Test against strings, objects and arrays. 1`] = ` - arrayObject + name : - [{"object1":"the-object-1"},{"object2":"the-objects-2"}] + the-name    - arrayString + nested.inner1 : - ["the-array-string-1","the-array-string-2"] + the-inner-1    - name + nested.inner2 : - the-name + the-inner-2    - nested.inner1 + arrayString : - the-inner-1 + ["the-array-string-1","the-array-string-2"]    - nested.inner2 + arrayObject : - the-inner-2 + [{"object1":"the-object-1"},{"object2":"the-objects-2"}]    diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx index b3380df0a7cb9f..611ae1b892dcc5 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx @@ -7,22 +7,38 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { getNestedProperty } from '../../../../../util/object_utils'; +import { getFlattenedFields } from '../../../../common'; + import { ExpandedRow } from './expanded_row'; describe('Data Frame: ', () => { test('Test against strings, objects and arrays.', () => { + const source = { + name: 'the-name', + nested: { + inner1: 'the-inner-1', + inner2: 'the-inner-2', + }, + arrayString: ['the-array-string-1', 'the-array-string-2'], + arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], + } as Record; + + const flattenedSource = getFlattenedFields(source).reduce( + (p, c) => { + p[c] = getNestedProperty(source, c); + if (p[c] === undefined) { + p[c] = source[`"${c}"`]; + } + return p; + }, + {} as Record + ); + const props = { item: { _id: 'the-id', - _source: { - name: 'the-name', - nested: { - inner1: 'the-inner-1', - inner2: 'the-inner-2', - }, - arrayString: ['the-array-string-1', 'the-array-string-2'], - arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], - }, + _source: flattenedSource, }, }; diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx index f765059a9d95f8..c69d0513486548 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx @@ -7,25 +7,16 @@ import React from 'react'; import { EuiBadge, EuiText } from '@elastic/eui'; -import { idx } from '@kbn/elastic-idx'; -import { getSelectableFields, EsDoc } from '../../../../common'; +import { EsDoc } from '../../../../common'; -interface ExpandedRowProps { - item: EsDoc; -} - -export const ExpandedRow: React.SFC = ({ item }) => { - const keys = getSelectableFields([item]); - const list = keys.map(k => { - // split the attribute key string and use reduce with an idx check to access nested attributes. - const value = k.split('.').reduce((obj, i) => idx(obj, _ => _[i]), item._source) || ''; - return ( +export const ExpandedRow: React.SFC<{ item: EsDoc }> = ({ item }) => ( + + {Object.entries(item._source).map(([k, value]) => ( {k}: {typeof value === 'string' ? value : JSON.stringify(value)}   - ); - }); - return {list}; -}; + ))} + +); diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts index bc7d0ada99b19b..f873d28371eaa7 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts @@ -8,11 +8,10 @@ import React, { useEffect, useState } from 'react'; import { SearchResponse } from 'elasticsearch'; -import { idx } from '@kbn/elastic-idx'; - import { StaticIndexPattern } from 'ui/index_patterns'; import { ml } from '../../../../../services/ml_api_service'; +import { getNestedProperty } from '../../../../../util/object_utils'; import { getDefaultSelectableFields, @@ -82,8 +81,11 @@ export const useSourceIndexData = ( [key: string]: any; }; flattenedFields.forEach(ff => { - item[ff] = idx(doc._source, _ => _[ff]); + item[ff] = getNestedProperty(doc._source, ff); if (item[ff] === undefined) { + // If the attribute is undefined, it means it was not a nested property + // but had dots in its actual name. This selects the property by its + // full name and assigns it to `item[ff]`. item[ff] = doc._source[`"${ff}"`]; } }); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 95c81e20e6ccb3..d3ddc5999e7d3e 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -63,7 +63,11 @@ function stringMatch(str: string | undefined, substr: string) { ); } -export const DataFrameAnalyticsList: FC = () => { +interface Props { + isManagementTable?: boolean; +} +// isManagementTable - for use in Kibana managagement ML section +export const DataFrameAnalyticsList: FC = ({ isManagementTable }) => { const [isInitialized, setIsInitialized] = useState(false); const [isLoading, setIsLoading] = useState(false); const [blockRefresh, setBlockRefresh] = useState(false); @@ -225,7 +229,7 @@ export const DataFrameAnalyticsList: FC = () => { ); } - const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds); + const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds, isManagementTable); const sorting = { sort: { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 3549732b0edc6a..4362a0f8d71064 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -59,7 +59,8 @@ export const getTaskStateBadge = ( export const getColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], - setExpandedRowItemIds: React.Dispatch> + setExpandedRowItemIds: React.Dispatch>, + isManagementTable: boolean = false ) => { const actions = getActions(); @@ -75,8 +76,8 @@ export const getColumns = ( // spread to a new array otherwise the component wouldn't re-render setExpandedRowItemIds([...expandedRowItemIds]); } - - return [ + // update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI + const columns: any[] = [ { align: RIGHT_ALIGNMENT, width: '40px', @@ -205,12 +206,17 @@ export const getColumns = ( }, width: '100px', }, - { + ]; + + if (isManagementTable === false) { + columns.push({ name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', { defaultMessage: 'Actions', }), actions, width: '200px', - }, - ]; + }); + } + + return columns; }; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index 4a6f025669acf0..20b9df34b4aafe 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -32,7 +32,7 @@ export const CreateAnalyticsButton: FC = () => { data-test-subj="mlDataFrameAnalyticsButtonCreate" > {i18n.translate('xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton', { - defaultMessage: 'Create data frame analytics job', + defaultMessage: 'Create outlier detection job', })} ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx index 0c905b67976526..2f5f9c944ef654 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx @@ -35,7 +35,7 @@ export const CreateAnalyticsModal: FC = ({ {i18n.translate('xpack.ml.dataframe.analytics.create.modalHeaderTitle', { - defaultMessage: 'Create data frame analytics job', + defaultMessage: 'Create outlier detection job', })} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts new file mode 100644 index 00000000000000..9df0b542f50a12 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/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 { useCreateAnalyticsForm, CreateAnalyticsFormProps } from './use_create_analytics_form'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts new file mode 100644 index 00000000000000..8324f41872d4d1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { getInitialState, reducer, ACTION } from './reducer'; + +describe('useCreateAnalyticsForm', () => { + test('reducer(): provide a minimum required valid job config, then reset.', () => { + const initialState = getInitialState(); + expect(initialState.isValid).toBe(false); + + const updatedState = reducer(initialState, { + type: ACTION.SET_FORM_STATE, + payload: { + destinationIndex: 'the-destination-index', + jobId: 'the-analytics-job-id', + sourceIndex: 'the-source-index', + }, + }); + expect(updatedState.isValid).toBe(true); + + const resettedState = reducer(updatedState, { + type: ACTION.RESET_FORM, + }); + expect(resettedState).toEqual(initialState); + }); + + test('reducer(): open/close the modal', () => { + const initialState = getInitialState(); + expect(initialState.isModalVisible).toBe(false); + + const openModalState = reducer(initialState, { + type: ACTION.OPEN_MODAL, + }); + expect(openModalState.isModalVisible).toBe(true); + + const closedModalState = reducer(openModalState, { + type: ACTION.CLOSE_MODAL, + }); + expect(closedModalState.isModalVisible).toBe(false); + }); + + test('reducer(): add/reset request messages', () => { + const initialState = getInitialState(); + expect(initialState.requestMessages).toHaveLength(0); + + const requestMessageState = reducer(initialState, { + type: ACTION.ADD_REQUEST_MESSAGE, + requestMessage: { + message: 'the-message', + }, + }); + expect(requestMessageState.requestMessages).toHaveLength(1); + + const resetMessageState = reducer(requestMessageState, { + type: ACTION.RESET_REQUEST_MESSAGES, + }); + expect(resetMessageState.requestMessages).toHaveLength(0); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts new file mode 100644 index 00000000000000..68fd46a69fee59 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -0,0 +1,179 @@ +/* + * 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 { isValidIndexName } from '../../../../../../common/util/es_utils'; +import { checkPermission } from '../../../../../privilege/check_privilege'; + +import { isAnalyticsIdValid, DataFrameAnalyticsId } from '../../../../common'; + +export type EsIndexName = string; +export type IndexPatternTitle = string; + +export interface RequestMessage { + error?: string; + message: string; +} + +export interface State { + createIndexPattern: boolean; + destinationIndex: EsIndexName; + destinationIndexNameExists: boolean; + destinationIndexNameEmpty: boolean; + destinationIndexNameValid: boolean; + destinationIndexPatternTitleExists: boolean; + disabled: boolean; + indexNames: EsIndexName[]; + indexPatternTitles: IndexPatternTitle[]; + indexPatternsWithNumericFields: IndexPatternTitle[]; + isJobCreated: boolean; + isJobStarted: boolean; + isModalButtonDisabled: boolean; + isModalVisible: boolean; + isValid: boolean; + jobId: DataFrameAnalyticsId; + jobIds: DataFrameAnalyticsId[]; + jobIdExists: boolean; + jobIdEmpty: boolean; + jobIdValid: boolean; + requestMessages: RequestMessage[]; + sourceIndex: EsIndexName; + sourceIndexNameExists: boolean; + sourceIndexNameEmpty: boolean; + sourceIndexNameValid: boolean; +} + +export const getInitialState = (): State => ({ + createIndexPattern: false, + destinationIndex: '', + destinationIndexNameExists: false, + destinationIndexNameEmpty: true, + destinationIndexNameValid: false, + destinationIndexPatternTitleExists: false, + disabled: + !checkPermission('canCreateDataFrameAnalytics') || + !checkPermission('canStartStopDataFrameAnalytics'), + indexNames: [], + indexPatternTitles: [], + indexPatternsWithNumericFields: [], + isJobCreated: false, + isJobStarted: false, + isModalVisible: false, + isModalButtonDisabled: false, + isValid: false, + jobId: '', + jobIds: [], + jobIdExists: false, + jobIdEmpty: true, + jobIdValid: false, + requestMessages: [], + sourceIndex: '', + sourceIndexNameExists: false, + sourceIndexNameEmpty: true, + sourceIndexNameValid: false, +}); + +const validate = (state: State): State => { + state.isValid = + !state.jobIdEmpty && + state.jobIdValid && + !state.jobIdExists && + !state.sourceIndexNameEmpty && + state.sourceIndexNameValid && + !state.destinationIndexNameEmpty && + state.destinationIndexNameValid && + (!state.destinationIndexPatternTitleExists || !state.createIndexPattern); + + return state; +}; + +export enum ACTION { + ADD_REQUEST_MESSAGE = 'add_request_message', + RESET_REQUEST_MESSAGES = 'reset_request_messages', + CLOSE_MODAL = 'close_modal', + OPEN_MODAL = 'open_modal', + RESET_FORM = 'reset_form', + SET_FORM_STATE = 'set_form_state', +} + +export type Action = + | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: RequestMessage } + | { type: ACTION.RESET_REQUEST_MESSAGES } + | { type: ACTION.CLOSE_MODAL } + | { type: ACTION.OPEN_MODAL } + | { type: ACTION.RESET_FORM } + | { type: ACTION.SET_FORM_STATE; payload: Partial }; + +export function reducer(state: State, action: Action): State { + switch (action.type) { + case ACTION.ADD_REQUEST_MESSAGE: + state.requestMessages.push(action.requestMessage); + return state; + + case ACTION.RESET_REQUEST_MESSAGES: + return { ...state, requestMessages: [] }; + + case ACTION.CLOSE_MODAL: + return { ...state, isModalVisible: false }; + + case ACTION.OPEN_MODAL: + return { ...state, isModalVisible: true }; + + case ACTION.RESET_FORM: + return getInitialState(); + + case ACTION.SET_FORM_STATE: + const newState = { ...state, ...action.payload }; + + // update state attributes which are derived from other state attributes. + if (action.payload.destinationIndex !== undefined) { + newState.destinationIndexNameExists = newState.indexNames.some( + name => newState.destinationIndex === name + ); + newState.destinationIndexNameEmpty = newState.destinationIndex === ''; + newState.destinationIndexNameValid = isValidIndexName(newState.destinationIndex); + newState.destinationIndexPatternTitleExists = newState.indexPatternTitles.some( + name => newState.destinationIndex === name + ); + } + + if (action.payload.indexNames !== undefined) { + newState.destinationIndexNameExists = newState.indexNames.some( + name => newState.destinationIndex === name + ); + newState.sourceIndexNameExists = newState.indexNames.some( + name => newState.sourceIndex === name + ); + } + + if (action.payload.indexPatternTitles !== undefined) { + newState.destinationIndexPatternTitleExists = newState.indexPatternTitles.some( + name => newState.destinationIndex === name + ); + } + + if (action.payload.jobId !== undefined) { + newState.jobIdExists = newState.jobIds.some(id => newState.jobId === id); + newState.jobIdEmpty = newState.jobId === ''; + newState.jobIdValid = isAnalyticsIdValid(newState.jobId); + } + + if (action.payload.jobIds !== undefined) { + newState.jobIdExists = newState.jobIds.some(id => newState.jobId === id); + } + + if (action.payload.sourceIndex !== undefined) { + newState.sourceIndexNameExists = newState.indexNames.some( + name => newState.sourceIndex === name + ); + newState.sourceIndexNameEmpty = newState.sourceIndex === ''; + newState.sourceIndexNameValid = isValidIndexName(newState.sourceIndex); + } + + return validate(newState); + } + + return state; +} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx new file mode 100644 index 00000000000000..4985c369ea2c68 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { mountHook } from 'test_utils/enzyme_helpers'; + +import { KibanaContext } from '../../../../../contexts/kibana'; +import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; + +import { getErrorMessage, useCreateAnalyticsForm } from './use_create_analytics_form'; + +const getMountedHook = () => + mountHook( + () => useCreateAnalyticsForm(), + ({ children }) => ( + {children} + ) + ); + +describe('getErrorMessage()', () => { + test('verify error message response formats', () => { + const errorMessage = getErrorMessage(new Error('the-error-message')); + expect(errorMessage).toBe('the-error-message'); + + const customError1 = { customErrorMessage: 'the-error-message' }; + const errorMessageMessage1 = getErrorMessage(customError1); + expect(errorMessageMessage1).toBe('{"customErrorMessage":"the-error-message"}'); + + const customError2 = { message: 'the-error-message' }; + const errorMessageMessage2 = getErrorMessage(customError2); + expect(errorMessageMessage2).toBe('the-error-message'); + }); +}); + +describe('useCreateAnalyticsForm()', () => { + test('initialization', () => { + const { getLastHookValue } = getMountedHook(); + const { state, actions } = getLastHookValue(); + + expect(state.isModalVisible).toBe(false); + expect(typeof actions.closeModal).toBe('function'); + expect(typeof actions.createAnalyticsJob).toBe('function'); + expect(typeof actions.openModal).toBe('function'); + expect(typeof actions.startAnalyticsJob).toBe('function'); + expect(typeof actions.setFormState).toBe('function'); + }); + + test('open/close modal', () => { + const { act, getLastHookValue } = getMountedHook(); + const { state, actions } = getLastHookValue(); + + expect(state.isModalVisible).toBe(false); + + act(() => { + // this should be actions.openModal(), but that doesn't work yet because act() doesn't support async yet. + // we need to wait for an update to React 16.9 + actions.setFormState({ isModalVisible: true }); + }); + const { state: stateModalOpen } = getLastHookValue(); + expect(stateModalOpen.isModalVisible).toBe(true); + + act(() => { + // this should be actions.closeModal(), but that doesn't work yet because act() doesn't support async yet. + // we need to wait for an update to React 16.9 + actions.setFormState({ isModalVisible: false }); + }); + const { state: stateModalClosed } = getLastHookValue(); + expect(stateModalClosed.isModalVisible).toBe(false); + }); + + // TODO + // add tests for createAnalyticsJob() and startAnalyticsJob() + // once React 16.9 with support for async act() is available. +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts similarity index 60% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form.ts rename to x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index e443187e5e8fd7..1205b9650be87f 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -8,115 +8,19 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; -import { checkPermission } from '../../../../privilege/check_privilege'; -import { ml } from '../../../../services/ml_api_service'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { ml } from '../../../../../services/ml_api_service'; +import { useKibanaContext } from '../../../../../contexts/kibana'; -import { isValidIndexName } from '../../../../../common/util/es_utils'; +import { useRefreshAnalyticsList, DataFrameAnalyticsOutlierConfig } from '../../../../common'; import { - isAnalyticsIdValid, - useRefreshAnalyticsList, - DataFrameAnalyticsId, - DataFrameAnalyticsOutlierConfig, -} from '../../../common'; - -export type EsIndexName = string; -export type IndexPatternTitle = string; - -interface RequestMessage { - error?: string; - message: string; -} - -interface State { - createIndexPattern: boolean; - destinationIndex: EsIndexName; - destinationIndexNameExists: boolean; - destinationIndexNameEmpty: boolean; - destinationIndexNameValid: boolean; - destinationIndexPatternTitleExists: boolean; - disabled: boolean; - indexNames: EsIndexName[]; - indexPatternTitles: IndexPatternTitle[]; - indexPatternsWithNumericFields: IndexPatternTitle[]; - isJobCreated: boolean; - isJobStarted: boolean; - isModalButtonDisabled: boolean; - isModalVisible: boolean; - isValid: boolean; - jobId: DataFrameAnalyticsId; - jobIds: DataFrameAnalyticsId[]; - jobIdExists: boolean; - jobIdEmpty: boolean; - jobIdValid: boolean; - requestMessages: RequestMessage[]; - sourceIndex: EsIndexName; - sourceIndexNameExists: boolean; - sourceIndexNameEmpty: boolean; - sourceIndexNameValid: boolean; -} - -export const getInitialState = (): State => ({ - createIndexPattern: false, - destinationIndex: '', - destinationIndexNameExists: false, - destinationIndexNameEmpty: true, - destinationIndexNameValid: false, - destinationIndexPatternTitleExists: false, - disabled: - !checkPermission('canCreateDataFrameAnalytics') || - !checkPermission('canStartStopDataFrameAnalytics'), - indexNames: [], - indexPatternTitles: [], - indexPatternsWithNumericFields: [], - isJobCreated: false, - isJobStarted: false, - isModalVisible: false, - isModalButtonDisabled: false, - isValid: false, - jobId: '', - jobIds: [], - jobIdExists: false, - jobIdEmpty: true, - jobIdValid: false, - requestMessages: [], - sourceIndex: '', - sourceIndexNameExists: false, - sourceIndexNameEmpty: true, - sourceIndexNameValid: false, -}); - -const validate = (state: State): State => { - state.isValid = - !state.jobIdEmpty && - state.jobIdValid && - !state.jobIdExists && - !state.sourceIndexNameEmpty && - state.sourceIndexNameValid && - !state.destinationIndexNameEmpty && - state.destinationIndexNameValid && - (!state.destinationIndexPatternTitleExists || !state.createIndexPattern); - - return state; -}; - -enum ACTION { - ADD_REQUEST_MESSAGE = 'add_request_message', - RESET_REQUEST_MESSAGES = 'reset_request_messages', - CLOSE_MODAL = 'close_modal', - OPEN_MODAL = 'open_modal', - RESET_FORM = 'reset_form', - SET_FORM_STATE = 'set_form_state', -} - -type Action = - | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: RequestMessage } - | { type: ACTION.RESET_REQUEST_MESSAGES } - | { type: ACTION.CLOSE_MODAL } - | { type: ACTION.OPEN_MODAL } - | { type: ACTION.RESET_FORM } - | { type: ACTION.SET_FORM_STATE; payload: Partial }; + getInitialState, + reducer, + IndexPatternTitle, + RequestMessage, + State, + ACTION, +} from './reducer'; export interface Actions { closeModal: () => void; @@ -131,82 +35,10 @@ export interface CreateAnalyticsFormProps { formState: State; } -export function reducer(state: State, action: Action): State { - switch (action.type) { - case ACTION.ADD_REQUEST_MESSAGE: - state.requestMessages.push(action.requestMessage); - return state; - - case ACTION.RESET_REQUEST_MESSAGES: - return { ...state, requestMessages: [] }; - - case ACTION.CLOSE_MODAL: - return { ...state, isModalVisible: false }; - - case ACTION.OPEN_MODAL: - return { ...state, isModalVisible: true }; - - case ACTION.RESET_FORM: - return getInitialState(); - - case ACTION.SET_FORM_STATE: - const newState = { ...state, ...action.payload }; - - // update state attributes which are derived from other state attributes. - if (action.payload.destinationIndex !== undefined) { - newState.destinationIndexNameExists = newState.indexNames.some( - name => newState.destinationIndex === name - ); - newState.destinationIndexNameEmpty = newState.destinationIndex === ''; - newState.destinationIndexNameValid = isValidIndexName(newState.destinationIndex); - newState.destinationIndexPatternTitleExists = newState.indexPatternTitles.some( - name => newState.destinationIndex === name - ); - } - - if (action.payload.indexNames !== undefined) { - newState.destinationIndexNameExists = newState.indexNames.some( - name => newState.destinationIndex === name - ); - newState.sourceIndexNameExists = newState.indexNames.some( - name => newState.sourceIndex === name - ); - } - - if (action.payload.indexPatternTitles !== undefined) { - newState.destinationIndexPatternTitleExists = newState.indexPatternTitles.some( - name => newState.destinationIndex === name - ); - } - - if (action.payload.jobId !== undefined) { - newState.jobIdExists = newState.jobIds.some(id => newState.jobId === id); - newState.jobIdEmpty = newState.jobId === ''; - newState.jobIdValid = isAnalyticsIdValid(newState.jobId); - } - - if (action.payload.jobIds !== undefined) { - newState.jobIdExists = newState.jobIds.some(id => newState.jobId === id); - } - - if (action.payload.sourceIndex !== undefined) { - newState.sourceIndexNameExists = newState.indexNames.some( - name => newState.sourceIndex === name - ); - newState.sourceIndexNameEmpty = newState.sourceIndex === ''; - newState.sourceIndexNameValid = isValidIndexName(newState.sourceIndex); - } - - return validate(newState); - } - - return state; -} - // List of system fields we want to ignore for the numeric field check. const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; -function getErrorMessage(error: any) { +export function getErrorMessage(error: any) { if (typeof error === 'object' && typeof error.message === 'string') { return error.message; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js index ab84ee7dbd795e..a5519314c7c683 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js @@ -80,8 +80,12 @@ class CreateWatchFlyoutUI extends Component { } } - closeFlyout = () => { - this.setState({ isFlyoutVisible: false }); + closeFlyout = (watchCreated = false) => { + this.setState({ isFlyoutVisible: false }, ()=>{ + if (typeof this.props.flyoutHidden === 'function') { + this.props.flyoutHidden(watchCreated); + } + }); } showFlyout = (jobId) => { @@ -107,7 +111,7 @@ class CreateWatchFlyoutUI extends Component { mlCreateWatchService.createNewWatch(this.state.jobId) .then((resp) => { toastNotifications.addSuccess(getSuccessToast(resp.id, resp.url, intl)); - this.closeFlyout(); + this.closeFlyout(true); }) .catch((error) => { toastNotifications.addDanger(intl.formatMessage({ @@ -194,6 +198,7 @@ class CreateWatchFlyoutUI extends Component { CreateWatchFlyoutUI.propTypes = { setShowFunction: PropTypes.func.isRequired, unsetShowFunction: PropTypes.func.isRequired, + flyoutHidden: PropTypes.func, }; export const CreateWatchFlyout = injectI18n(CreateWatchFlyoutUI); diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js index d3145cb7978642..1a6243413d614b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -80,6 +80,11 @@ class JobsListUI extends Component { }; getJobIdLink(id) { + // Don't allow link to job if ML is not enabled in current space + if (this.props.isMlEnabledInSpace === false) { + return id; + } + return ( {id} @@ -253,6 +258,10 @@ class JobsListUI extends Component { {'all'} ) }); + // Remove actions if Ml not enabled in current space + if (this.props.isMlEnabledInSpace === false) { + columns.pop(); + } } else { // insert before last column columns.splice(columns.length - 1, 0, { @@ -344,6 +353,8 @@ class JobsListUI extends Component { JobsListUI.propTypes = { jobsSummaryList: PropTypes.array.isRequired, fullJobsList: PropTypes.object.isRequired, + isManagementTable: PropTypes.bool, + isMlEnabledInSpace: PropTypes.bool, itemIdToExpandedRowMap: PropTypes.object.isRequired, toggleRow: PropTypes.func.isRequired, selectJobChange: PropTypes.func.isRequired, @@ -355,6 +366,8 @@ JobsListUI.propTypes = { loading: PropTypes.bool, }; JobsListUI.defaultProps = { + isManagementTable: false, + isMlEnabledInSpace: true, loading: false, }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 337bb5a12f61cf..fa55ef0fdda0c1 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -362,21 +362,22 @@ export class JobsListView extends Component { } renderManagementJobsListComponents() { - const { loading } = this.state; + const { loading, itemIdToExpandedRowMap, filteredJobsSummaryList, fullJobsList, selectedJobs } = this.state; return (
); @@ -430,7 +431,6 @@ export class JobsListView extends Component {
); diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js index d0a44549c53338..e149261b7971d4 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js @@ -8,7 +8,7 @@ import { each } from 'lodash'; import { toastNotifications } from 'ui/notify'; import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; import rison from 'rison-node'; -import chrome from 'ui/chrome'; // TODO: get from context once walter's PR is merged +import chrome from 'ui/chrome'; import { mlJobService } from 'plugins/ml/services/job_service'; import { ml } from 'plugins/ml/services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts index baeec2dd6576b6..9c1e339c9bbe4c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts @@ -95,11 +95,19 @@ export class JobCreator { return agg !== undefined ? agg : null; } + public get aggregations(): Aggregation[] { + return this._aggs; + } + public getField(index: number): Field | null { const field = this._fields[index]; return field !== undefined ? field : null; } + public get fields(): Field[] { + return this._fields; + } + public set bucketSpan(bucketSpan: BucketSpan) { this._job_config.analysis_config.bucket_span = bucketSpan; } @@ -247,11 +255,12 @@ export class JobCreator { return this._subscribers; } - public async createAndStartJob() { + public async createAndStartJob(): Promise { try { await this.createJob(); await this.createDatafeed(); - await this.startDatafeed(); + const jobRunner = await this.startDatafeed(); + return jobRunner; } catch (error) { throw error; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts index c44cadca33adb4..df28af6b83c6a1 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts @@ -141,6 +141,7 @@ export class MultiMetricJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.jobId = ''; + this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; const detectors = getRichDetectors(job.analysis_config.detectors); this.removeAllDetectors(); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts index d567e108e02e81..1f6d3392cfa0c9 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts @@ -130,6 +130,7 @@ export class PopulationJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.jobId = ''; + this.createdBy = CREATED_BY_LABEL.POPULATION; const detectors = getRichDetectors(job.analysis_config.detectors); this.removeAllDetectors(); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts index a2ba0801e33246..c38c52681fd318 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts @@ -181,6 +181,7 @@ export class SingleMetricJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.jobId = ''; + this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; const detectors = getRichDetectors(job.analysis_config.detectors); this.removeAllDetectors(); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts index 3f8bdfa371d01f..830cfb713ac2d8 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts @@ -66,10 +66,20 @@ export class JobRunner { // start the datafeed and then start polling for progress // the complete percentage is added to an observable // so all pre-subscribed listeners can follow along. - public async startDatafeed(): Promise { + private async _startDatafeed( + start: number | undefined, + end: number | undefined, + pollProgress: boolean + ): Promise { try { await this.openJob(); - await mlJobService.startDatafeed(this._datafeedId, this._jobId, this._start, this._end); + const { started } = await mlJobService.startDatafeed( + this._datafeedId, + this._jobId, + start, + end + ); + this._datafeedState = DATAFEED_STATE.STARTED; this._percentageComplete = 0; @@ -87,12 +97,25 @@ export class JobRunner { }; // wait for the first check to run and then return success. // all subsequent checks will update the observable - await check(); + if (pollProgress === true) { + await check(); + } + return started; } catch (error) { throw error; } } + public async startDatafeed() { + return await this._startDatafeed(this._start, this._end, true); + } + + public async startDatafeedInRealTime(continueJob: boolean) { + // if continuing a job, set the start to be the end date + const start = continueJob ? this._end : this._start; + return await this._startDatafeed(start, undefined, false); + } + public async getProgress(): Promise<{ progress: Progress; isRunning: boolean }> { return await ml.jobs.getLookBackProgress(this._jobId, this._start, this._end); } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx index eace905b2c6de4..dfe9272984b814 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx @@ -5,12 +5,18 @@ */ import React, { FC, useContext, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { BucketSpanInput } from './bucket_span_input'; import { JobCreatorContext } from '../../../job_creator_context'; import { Description } from './description'; +import { BucketSpanEstimator } from '../bucket_span_estimator'; -export const BucketSpan: FC = () => { +interface Props { + setIsValid: (proceed: boolean) => void; +} + +export const BucketSpan: FC = ({ setIsValid }) => { const { jobCreator, jobCreatorUpdate, @@ -20,6 +26,7 @@ export const BucketSpan: FC = () => { } = useContext(JobCreatorContext); const [bucketSpan, setBucketSpan] = useState(jobCreator.bucketSpan); const [validation, setValidation] = useState(jobValidator.bucketSpan); + const [estimating, setEstimating] = useState(false); useEffect(() => { jobCreator.bucketSpan = bucketSpan; @@ -34,13 +41,25 @@ export const BucketSpan: FC = () => { setValidation(jobValidator.bucketSpan); }, [jobValidatorUpdated]); + useEffect(() => { + setIsValid(estimating === false); + }, [estimating]); + return ( - + + + + + + + + ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx index 4ad8cc05c4e8e7..7c1aaca9dc0c2d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx @@ -11,11 +11,13 @@ interface Props { bucketSpan: string; setBucketSpan: (bs: string) => void; isInvalid: boolean; + disabled: boolean; } -export const BucketSpanInput: FC = ({ bucketSpan, setBucketSpan, isInvalid }) => { +export const BucketSpanInput: FC = ({ bucketSpan, setBucketSpan, isInvalid, disabled }) => { return ( setBucketSpan(e.target.value)} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx new file mode 100644 index 00000000000000..a61d5c39f8294f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.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, { FC, useEffect } from 'react'; +import { EuiButton } from '@elastic/eui'; + +import { useEstimateBucketSpan, ESTIMATE_STATUS } from './estimate_bucket_span'; + +interface Props { + setEstimating(estimating: boolean): void; +} + +export const BucketSpanEstimator: FC = ({ setEstimating }) => { + const { status, estimateBucketSpan } = useEstimateBucketSpan(); + + useEffect(() => { + setEstimating(status === ESTIMATE_STATUS.RUNNING); + }, [status]); + + return ( + + Estimate bucket span + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts new file mode 100644 index 00000000000000..79b72121732e38 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -0,0 +1,70 @@ +/* + * 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 { useContext, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { toastNotifications } from 'ui/notify'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields'; +import { isMultiMetricJobCreator, isPopulationJobCreator } from '../../../../../common/job_creator'; +import { ml } from '../../../../../../../services/ml_api_service'; +import { useKibanaContext } from '../../../../../../../contexts/kibana'; + +export enum ESTIMATE_STATUS { + NOT_RUNNING, + RUNNING, +} + +export function useEstimateBucketSpan() { + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); + const kibanaContext = useKibanaContext(); + + const [status, setStatus] = useState(ESTIMATE_STATUS.NOT_RUNNING); + + const data = { + aggTypes: jobCreator.aggregations.map(a => a.dslName), + duration: { + start: jobCreator.start, + end: jobCreator.end, + }, + fields: jobCreator.fields.map(f => (f.id === EVENT_RATE_FIELD_ID ? null : f.id)), + index: kibanaContext.currentIndexPattern.title, + query: kibanaContext.combinedQuery, + splitField: + (isMultiMetricJobCreator(jobCreator) || isPopulationJobCreator(jobCreator)) && + jobCreator.splitField !== null + ? jobCreator.splitField.id + : undefined, + timeField: kibanaContext.currentIndexPattern.timeFieldName, + }; + + async function estimateBucketSpan() { + setStatus(ESTIMATE_STATUS.RUNNING); + const { name, error, message } = await ml.estimateBucketSpan(data); + setStatus(ESTIMATE_STATUS.NOT_RUNNING); + if (error === true) { + let text = ''; + if (message !== undefined) { + if (typeof message === 'object') { + text = message.msg || JSON.stringify(message); + } else { + text = message; + } + } + toastNotifications.addDanger({ + title: i18n.translate('xpack.ml.newJob.wizard.estimateBucketSpanError', { + defaultMessage: `Bucket span estimation error`, + }), + text, + }); + } else { + jobCreator.bucketSpan = name; + jobCreatorUpdate(); + } + } + return { status, estimateBucketSpan }; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts new file mode 100644 index 00000000000000..45e0d10fd29a5e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { BucketSpanEstimator } from './bucket_span_estimator'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx index 03cd9b2dee232c..8b3d718f880e97 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx @@ -45,7 +45,7 @@ export const MultiMetricSettings: FC = ({ isActive, setIsValid }) => {
- + diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx index ee010f89c94a22..61d8a5f358964a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx @@ -36,7 +36,7 @@ export const PopulationSettings: FC = ({ isActive, setIsValid }) => { - + diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx index 9d01494e59b723..1e6e137799d561 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx @@ -51,7 +51,7 @@ export const SingleMetricSettings: FC = ({ isActive, setIsValid }) => { - + diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/components/post_save_options/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/components/post_save_options/index.ts new file mode 100644 index 00000000000000..dacf9184843c49 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/components/post_save_options/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 { PostSaveOptions } from './post_save_options'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/components/post_save_options/post_save_options.tsx new file mode 100644 index 00000000000000..e1251ad74c4f01 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -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 React, { FC, Fragment, useContext, useState } from 'react'; +import { toastNotifications } from 'ui/notify'; +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { JobRunner } from '../../../../../common/job_runner'; + +// @ts-ignore +import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout'; +import { JobCreatorContext } from '../../../../components/job_creator_context'; +import { DATAFEED_STATE } from '../../../../../../../../common/constants/states'; + +interface Props { + jobRunner: JobRunner | null; +} + +type ShowFlyout = (jobId: string) => void; + +export const PostSaveOptions: FC = ({ jobRunner }) => { + const { jobCreator } = useContext(JobCreatorContext); + const [datafeedState, setDatafeedState] = useState(DATAFEED_STATE.STOPPED); + const [watchFlyoutVisible, setWatchFlyoutVisible] = useState(false); + const [watchCreated, setWatchCreated] = useState(false); + + function setShowCreateWatchFlyoutFunction(showFlyout: ShowFlyout) { + showFlyout(jobCreator.jobId); + } + + function flyoutHidden(jobCreated: boolean) { + setWatchFlyoutVisible(false); + setWatchCreated(jobCreated); + } + + function unsetShowCreateWatchFlyoutFunction() { + setWatchFlyoutVisible(false); + } + + async function startJobInRealTime() { + setDatafeedState(DATAFEED_STATE.STARTING); + if (jobRunner !== null) { + try { + const started = await jobRunner.startDatafeedInRealTime(true); + setDatafeedState(started === true ? DATAFEED_STATE.STARTED : DATAFEED_STATE.STOPPED); + toastNotifications.addSuccess({ + title: i18n.translate('xpack.ml.newJob.wizard.startJobInRealTimeSuccess', { + defaultMessage: `Job {jobId} started`, + values: { jobId: jobCreator.jobId }, + }), + }); + } catch (error) { + setDatafeedState(DATAFEED_STATE.STOPPED); + toastNotifications.addDanger({ + title: i18n.translate('xpack.ml.newJob.wizard.startJobInRealTimeError', { + defaultMessage: `Error starting job`, + }), + text: error.message, + }); + } + } + } + + return ( + +   + + + +   + setWatchFlyoutVisible(true)} + data-test-subj="mlButtonUseFullData" + > + + + {datafeedState === DATAFEED_STATE.STARTED && watchFlyoutVisible && ( + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx index 6812935794baf6..e89892f8883d26 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx @@ -11,12 +11,14 @@ import { toastNotifications } from 'ui/notify'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; +import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonFlyout } from './json_flyout'; import { isSingleMetricJobCreator } from '../../../common/job_creator'; import { JobDetails } from './job_details'; import { DetectorChart } from './detector_chart'; import { JobProgress } from './components/job_progress'; +import { PostSaveOptions } from './components/post_save_options'; export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobValidator, jobValidatorUpdated, resultsLoader } = useContext( @@ -24,7 +26,9 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => ); const [progress, setProgress] = useState(resultsLoader.progress); const [showJsonFlyout, setShowJsonFlyout] = useState(false); + const [creatingJob, setCreatingJob] = useState(false); const [isValid, setIsValid] = useState(jobValidator.validationSummary.basic); + const [jobRunner, setJobRunner] = useState(null); useEffect(() => { jobCreator.subscribeToProgress(setProgress); @@ -32,8 +36,10 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => async function start() { setShowJsonFlyout(false); + setCreatingJob(true); try { - await jobCreator.createAndStartJob(); + const jr = await jobCreator.createAndStartJob(); + setJobRunner(jr); } catch (error) { // catch and display all job creation errors toastNotifications.addDanger({ @@ -42,6 +48,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => }), text: error.message, }); + setCreatingJob(false); } } @@ -79,13 +86,16 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => 0} - disabled={isValid === false} + isDisabled={creatingJob === true || isValid === false} data-test-subj="mlJobWizardButtonCreateJob" > Create job   + + )} + {creatingJob === false && ( + = ({ setCurrentStep, isCurrentStep }) => {showJsonFlyout && ( setShowJsonFlyout(false)} jobCreator={jobCreator} /> )} -   )} {progress > 0 && ( @@ -105,6 +114,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => View results + {progress === 100 && ( + + + + )} )} diff --git a/x-pack/legacy/plugins/ml/public/management/index.ts b/x-pack/legacy/plugins/ml/public/management/index.ts index 08e17a2a44f734..744b03c3d592bc 100644 --- a/x-pack/legacy/plugins/ml/public/management/index.ts +++ b/x-pack/legacy/plugins/ml/public/management/index.ts @@ -15,9 +15,13 @@ import { management } from 'ui/management'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { i18n } from '@kbn/i18n'; import { JOBS_LIST_PATH } from './management_urls'; +import { LICENSE_TYPE } from '../../common/constants/license'; import 'plugins/ml/management/jobs_list'; -if (xpackInfo.get('features.ml.showLinks', false) === true) { +if ( + xpackInfo.get('features.ml.showLinks', false) === true && + xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL +) { management.register('ml', { display: i18n.translate('xpack.ml.management.mlTitle', { defaultMessage: 'Machine Learning', diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss index 9fdce0d98e5c4e..883ecd96745b42 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss @@ -1,3 +1,4 @@ @import './jobs_list_page/stats_bar'; @import './jobs_list_page/buttons'; @import './jobs_list_page/expanded_row'; +@import './jobs_list_page/analytics_table'; diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_analytics_table.scss b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_analytics_table.scss new file mode 100644 index 00000000000000..ce53c33ca9ece6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_analytics_table.scss @@ -0,0 +1,28 @@ + +.mlAnalyticsTable { + // Using an override as a last resort because we cannot set custom classes on + // nested upstream components. The opening animation limits the height + // of the expanded row to 1000px which turned out to be not predictable. + // The animation could also result in flickering with expanded rows + // where the inner content would result in the DOM changing the height. + .euiTableRow-isExpandedRow .euiTableCellContent { + animation: none !important; + .euiTableCellContent__text { + width: 100%; + } + } + // Another override: Because an update to the table replaces the DOM, the same + // icon would still again fade in with an animation. If the table refreshes with + // e.g. 1s this would result in a blinking icon effect. + .euiIcon-isLoaded { + animation: none !important; + } +} + +.mlAnalyticsProgressBar { + margin-bottom: $euiSizeM; +} + +.mlTaskStateBadge { + max-width: 100px; +} diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 852e33d92e5a83..6bdfa64833a394 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; import { @@ -19,8 +19,13 @@ import { // @ts-ignore undeclared module import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view'; +import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; -export const JobsListPage = () => { +interface Props { + isMlEnabledInSpace: boolean; +} + +export const JobsListPage: FC = ({ isMlEnabledInSpace }) => { const tabs = [ { id: 'anomaly_detection_jobs', @@ -30,23 +35,24 @@ export const JobsListPage = () => { content: ( - + + + ), + }, + { + id: 'analytics_jobs', + name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { + defaultMessage: 'Analytics', + }), + content: ( + + + ), }, - // { - // id: 'analytics_jobs', - // name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { - // defaultMessage: 'Analytics', - // }), - // content: renderAnalyticsJobs(), - // }, ]; - // function renderAnalyticsJobs() { - // return
Analytics job placeholder
; - // } - function renderTabs() { return ; } diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts b/x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts index 071d5ab6c6dfab..b88138d139f607 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore no declaration module -import { ReactDOM, render, unmountComponentAtNode } from 'react-dom'; +import ReactDOM, { render, unmountComponentAtNode } from 'react-dom'; +import React from 'react'; import routes from 'ui/routes'; import { canGetManagementMlJobs } from '../../privilege/check_privilege'; import { JOBS_LIST_PATH, ACCESS_DENIED_PATH } from '../management_urls'; @@ -22,14 +22,19 @@ routes.when(JOBS_LIST_PATH, { resolve: { checkPrivilege: canGetManagementMlJobs, }, - controller($scope) { + controller($scope, checkPrivilege) { + const { mlFeatureEnabledInSpace } = checkPrivilege; + $scope.$on('$destroy', () => { const elem = document.getElementById('kibanaManagementMLSection'); if (elem) unmountComponentAtNode(elem); }); $scope.$$postDigest(() => { const element = document.getElementById('kibanaManagementMLSection'); - render(JobsListPage(), element); + ReactDOM.render( + React.createElement(JobsListPage, { isMlEnabledInSpace: mlFeatureEnabledInSpace }), + element + ); }); }, }); diff --git a/x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts b/x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts index 8ea4b39ffe2f60..7f2985eda9da89 100644 --- a/x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts +++ b/x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts @@ -17,18 +17,20 @@ let privileges: Privileges = getDefaultPrivileges(); // manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53 export function canGetManagementMlJobs(kbnUrl: any) { return new Promise((resolve, reject) => { - getManageMlPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; - // Loop through all privilages to ensure they are all set to true. - const isManageML = Object.values(privileges).every(p => p === true); + getManageMlPrivileges().then( + ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { + privileges = capabilities; + // Loop through all privilages to ensure they are all set to true. + const isManageML = Object.values(privileges).every(p => p === true); - if (isManageML === true && isPlatinumOrTrialLicense === true) { - return resolve(); - } else { - kbnUrl.redirect(ACCESS_DENIED_PATH); - return reject(); + if (isManageML === true && isPlatinumOrTrialLicense === true) { + return resolve({ mlFeatureEnabledInSpace }); + } else { + kbnUrl.redirect(ACCESS_DENIED_PATH); + return reject(); + } } - }); + ); }); } diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts index d206f3748b36fc..d32e55a58f3a49 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts @@ -20,7 +20,12 @@ declare interface JobService { cloneJob(job: any): any; openJob(jobId: string): Promise; saveNewDatafeed(datafeedConfig: any, jobId: string): Promise; - startDatafeed(datafeedId: string, jobId: string, start: number, end: number): Promise; + startDatafeed( + datafeedId: string, + jobId: string, + start: number | undefined, + end: number | undefined + ): Promise; createResultsUrl(jobId: string[], start: number, end: number, location: string): string; getJobAndGroupIds(): ExistingJobsAndGroups; } diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 7433a38cc5e307..d2d40a9a54d31f 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -121,6 +121,10 @@ declare interface Ml { end: number ): Promise<{ progress: number; isRunning: boolean }>; }; + + estimateBucketSpan( + data: object + ): Promise<{ name: string; ms: number; error?: boolean; message?: { msg: string } | string }>; } declare const ml: Ml; diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts b/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts new file mode 100644 index 00000000000000..ffd0adcbe542da --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { getNestedProperty } from './object_utils'; + +describe('object_utils', () => { + test('getNestedProperty()', () => { + const testObj = { + the: { + nested: { + value: 'the-nested-value', + }, + }, + }; + + const test1 = getNestedProperty(testObj, 'the'); + expect(typeof test1).toBe('object'); + expect(Object.keys(test1)).toStrictEqual(['nested']); + + const test2 = getNestedProperty(testObj, 'the$'); + expect(typeof test2).toBe('undefined'); + + const test3 = getNestedProperty(testObj, 'the$', 'the-default-value'); + expect(typeof test3).toBe('string'); + expect(test3).toBe('the-default-value'); + + const test4 = getNestedProperty(testObj, 'the.neSted'); + expect(typeof test4).toBe('undefined'); + + const test5 = getNestedProperty(testObj, 'the.nested'); + expect(typeof test5).toBe('object'); + expect(Object.keys(test5)).toStrictEqual(['value']); + + const test6 = getNestedProperty(testObj, 'the.nested.vaLue'); + expect(typeof test6).toBe('undefined'); + + const test7 = getNestedProperty(testObj, 'the.nested.value'); + expect(typeof test7).toBe('string'); + expect(test7).toBe('the-nested-value'); + + const test8 = getNestedProperty(testObj, 'the.nested.value.doesntExist'); + expect(typeof test8).toBe('undefined'); + + const test9 = getNestedProperty(testObj, 'the.nested.value.doesntExist', 'the-default-value'); + expect(typeof test9).toBe('string'); + expect(test9).toBe('the-default-value'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.ts b/x-pack/legacy/plugins/ml/public/util/object_utils.ts new file mode 100644 index 00000000000000..1facc761d6379e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/object_utils.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { idx } from '@kbn/elastic-idx'; + +// This is similar to lodash's get() except that it's TypeScript aware and is able to infer return types. +// It splits the attribute key string and uses reduce with an idx check to access nested attributes. +export const getNestedProperty = ( + obj: Record, + accessor: string, + defaultValue?: any +) => { + return accessor.split('.').reduce((o, i) => idx(o, _ => _[i]), obj) || defaultValue; +}; diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts index ae73ba5be22bba..0ce60fffd836d5 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ b/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts @@ -26,7 +26,8 @@ interface Response { export function privilegesProvider( callWithRequest: callWithRequestType, xpackMainPlugin: XPackMainPlugin, - isMlEnabledInSpace: () => Promise + isMlEnabledInSpace: () => Promise, + ignoreSpaces: boolean = false ) { const { isUpgradeInProgress } = upgradeCheckProvider(callWithRequest); async function getPrivileges(): Promise { @@ -47,7 +48,7 @@ export function privilegesProvider( ? setFullActionPrivileges : setBasicActionPrivileges; - if (mlFeatureEnabledInSpace === false) { + if (mlFeatureEnabledInSpace === false && ignoreSpaces === false) { // if ML isn't enabled in the current space, // return with the default privileges (all false) return { diff --git a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts b/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts index 9c7a7b77a7e14a..175e0aee77eef4 100644 --- a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts +++ b/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts @@ -33,7 +33,7 @@ export function spacesUtilsProvider(spacesPlugin: any, request: Request, config: } } - async function isMlEnabled(): Promise { + async function isMlEnabledInSpace(): Promise { const { valid, space } = await activeSpace(); if (valid === true && space !== undefined) { return space.disabledFeatures.includes('ml') === false; @@ -41,5 +41,5 @@ export function spacesUtilsProvider(spacesPlugin: any, request: Request, config: return true; } - return { isMlEnabled }; + return { isMlEnabledInSpace }; } diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js b/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js index db1822c2eec646..59bfd564f7ca27 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js @@ -17,6 +17,7 @@ describe('ML - data recognizer', () => { 'apm_transaction', 'auditbeat_process_docker_ecs', 'auditbeat_process_hosts_ecs', + 'logs_ui_analysis', 'metricbeat_system_ecs', 'nginx_ecs', 'sample_data_ecommerce', diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json new file mode 100644 index 00000000000000..ead765e4747206 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json @@ -0,0 +1,3 @@ +{ + "icon": "loggingApp" +} diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json new file mode 100644 index 00000000000000..28fd590e683639 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json @@ -0,0 +1,20 @@ +{ + "id": "logs_ui_analysis", + "title": "Log Analysis", + "description": "Detect anomalies in log entries via the Logs UI", + "type": "Logs", + "logoFile": "logo.json", + "jobs": [ + { + "id": "log-entry-rate", + "file": "log_entry_rate.json" + } + ], + "datafeeds": [ + { + "id": "datafeed-log-entry-rate", + "file": "datafeed_log_entry_rate.json", + "job_id": "log-entry-rate" + } + ] +} diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json new file mode 100644 index 00000000000000..fbb0b6763e0453 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json @@ -0,0 +1,28 @@ +{ + "job_id": "JOB_ID", + "indexes": ["INDEX_PATTERN_NAME"], + "aggregations": { + "buckets": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "900000ms" + }, + "aggregations": { + "doc_count_per_minute": { + "bucket_script": { + "buckets_path": { + "doc_count": "_count" + }, + "script": { + "lang": "painless", + "params": { + "bucket_span_in_ms": 900000 + }, + "source": "60 * 1000 * params.doc_count / params.bucket_span_in_ms" + } + } + } + } + } + } +} diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json new file mode 100644 index 00000000000000..1e11bfa9a7f3b5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json @@ -0,0 +1,30 @@ +{ + "job_type": "anomaly_detector", + "description": "Detect anomalies in the log entry ingestion rate", + "groups": ["logs-ui"], + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name": "doc_count_per_minute", + "detectors": [ + { + "detector_description": "count", + "function": "count", + "detector_index": 0 + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "10mb" + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": true + }, + "custom_settings": { + "created_by": "ml-module-logs-ui-analysis" + } +} diff --git a/x-pack/legacy/plugins/ml/server/routes/system.js b/x-pack/legacy/plugins/ml/server/routes/system.js index 3eaa7b34b4ec1c..787fb87404581e 100644 --- a/x-pack/legacy/plugins/ml/server/routes/system.js +++ b/x-pack/legacy/plugins/ml/server/routes/system.js @@ -99,13 +99,12 @@ export function systemRoutes({ const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); try { const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; - const spacesFeature = xpackMainPlugin.info.feature('spaces'); - // if spaces is disabled or ignoreSpace is true force isMlEnabledInSpace to be true - const { isMlEnabledInSpace } = (spacesFeature.isEnabled() && ignoreSpaces === false) ? + // if spaces is disabled force isMlEnabledInSpace to be true + const { isMlEnabledInSpace } = spacesPlugin !== undefined ? spacesUtilsProvider(spacesPlugin, request, config) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider(callWithRequest, xpackMainPlugin, isMlEnabledInSpace); + const { getPrivileges } = privilegesProvider(callWithRequest, xpackMainPlugin, isMlEnabledInSpace, ignoreSpaces); return await getPrivileges(); } catch (error) { return wrapError(error); diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.test.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.test.ts new file mode 100644 index 00000000000000..caea9fd49fed6e --- /dev/null +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { DEFAULT_CSP_RULES } from '../../../../../../../../src/legacy/server/csp'; +import { + getMockCallWithInternal, + getMockKbnServer, + getMockTaskFetch, +} from '../../../../test_utils'; +import { createCspCollector } from './csp_collector'; + +test('fetches whether strict mode is enabled', async () => { + const { collector, mockConfig } = setupCollector(); + + expect((await collector.fetch()).strict).toEqual(true); + + mockConfig.get.withArgs('csp.strict').returns(false); + expect((await collector.fetch()).strict).toEqual(false); +}); + +test('fetches whether the legacy browser warning is enabled', async () => { + const { collector, mockConfig } = setupCollector(); + + expect((await collector.fetch()).warnLegacyBrowsers).toEqual(true); + + mockConfig.get.withArgs('csp.warnLegacyBrowsers').returns(false); + expect((await collector.fetch()).warnLegacyBrowsers).toEqual(false); +}); + +test('fetches whether the csp rules have been changed or not', async () => { + const { collector, mockConfig } = setupCollector(); + + expect((await collector.fetch()).rulesChangedFromDefault).toEqual(false); + + mockConfig.get.withArgs('csp.rules').returns(['not', 'default']); + expect((await collector.fetch()).rulesChangedFromDefault).toEqual(true); +}); + +test('does not include raw csp.rules under any property names', async () => { + const { collector } = setupCollector(); + + // It's important that we do not send the value of csp.rules here as it + // can be customized with values that can be identifiable to given + // installs, such as URLs + // + // We use a snapshot here to ensure csp.rules isn't finding its way into the + // payload under some new and unexpected variable name (e.g. cspRules). + expect(await collector.fetch()).toMatchInlineSnapshot(` + Object { + "rulesChangedFromDefault": false, + "strict": true, + "warnLegacyBrowsers": true, + } + `); +}); + +test('does not arbitrarily fetch other csp configurations (e.g. whitelist only)', async () => { + const { collector, mockConfig } = setupCollector(); + + mockConfig.get.withArgs('csp.foo').returns('bar'); + + expect(await collector.fetch()).not.toHaveProperty('foo'); +}); + +function setupCollector() { + const mockConfig = { get: sinon.stub() }; + mockConfig.get.withArgs('csp.rules').returns(DEFAULT_CSP_RULES); + mockConfig.get.withArgs('csp.strict').returns(true); + mockConfig.get.withArgs('csp.warnLegacyBrowsers').returns(true); + + const mockKbnServer = getMockKbnServer(getMockCallWithInternal(), getMockTaskFetch(), mockConfig); + + return { mockConfig, collector: createCspCollector(mockKbnServer) }; +} diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.ts new file mode 100644 index 00000000000000..3bacde22bdbc4c --- /dev/null +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.ts @@ -0,0 +1,39 @@ +/* + * 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 { + createCSPRuleString, + DEFAULT_CSP_RULES, +} from '../../../../../../../../src/legacy/server/csp'; +import { HapiServer } from '../../../../'; + +export function createCspCollector(server: HapiServer) { + return { + type: 'csp', + isReady: () => true, + async fetch() { + const config = server.config(); + + // It's important that we do not send the value of csp.rules here as it + // can be customized with values that can be identifiable to given + // installs, such as URLs + const defaultRulesString = createCSPRuleString([...DEFAULT_CSP_RULES]); + const actualRulesString = createCSPRuleString(config.get('csp.rules')); + + return { + strict: config.get('csp.strict'), + warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'), + rulesChangedFromDefault: defaultRulesString !== actualRulesString, + }; + }, + }; +} + +export function registerCspCollector(server: HapiServer): void { + const { usage } = server; + const collector = usage.collectorSet.makeUsageCollector(createCspCollector(server)); + usage.collectorSet.register(collector); +} diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/index.ts new file mode 100644 index 00000000000000..a7c1088f9961b2 --- /dev/null +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/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 { registerCspCollector } from './csp_collector'; diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts index 8b825b13178f20..abe96fe061faaa 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts @@ -5,8 +5,10 @@ */ import { HapiServer } from '../../../'; +import { registerCspCollector } from './csp'; import { registerVisualizationsCollector } from './visualizations/register_usage_collector'; export function registerCollectors(server: HapiServer) { registerVisualizationsCollector(server); + registerCspCollector(server); } diff --git a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts index 7659f0d3516f96..7168f598dca237 100644 --- a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts @@ -30,9 +30,16 @@ export const getMockTaskFetch = (docs: TaskInstance[] = defaultMockTaskDocs) => return () => Promise.resolve({ docs }); }; +export const getMockConfig = () => { + return { + get: () => '', + }; +}; + export const getMockKbnServer = ( mockCallWithInternal = getMockCallWithInternal(), - mockTaskFetch = getMockTaskFetch() + mockTaskFetch = getMockTaskFetch(), + mockConfig = getMockConfig() ): HapiServer => ({ plugins: { elasticsearch: { @@ -53,6 +60,6 @@ export const getMockKbnServer = ( register: () => undefined, }, }, - config: () => ({ get: () => '' }), + config: () => mockConfig, log: () => undefined, }); diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/cluster_serialization.test.ts b/x-pack/legacy/plugins/remote_clusters/common/cluster_serialization.test.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/cluster_serialization.test.ts rename to x-pack/legacy/plugins/remote_clusters/common/cluster_serialization.test.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/cluster_serialization.ts b/x-pack/legacy/plugins/remote_clusters/common/cluster_serialization.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/cluster_serialization.ts rename to x-pack/legacy/plugins/remote_clusters/common/cluster_serialization.ts diff --git a/x-pack/legacy/plugins/remote_clusters/common/index.ts b/x-pack/legacy/plugins/remote_clusters/common/index.ts index f869c0d2a55af3..8f80b3b7dc6a33 100644 --- a/x-pack/legacy/plugins/remote_clusters/common/index.ts +++ b/x-pack/legacy/plugins/remote_clusters/common/index.ts @@ -19,3 +19,5 @@ export const PLUGIN = { }; export const API_BASE_PATH = '/api/remote_clusters'; + +export { deserializeCluster, serializeCluster } from './cluster_serialization'; diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index c25c2e6995aed7..8209b792ec4d7d 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -284,33 +284,59 @@ Array [ class="euiSpacer euiSpacer--l" />,
+
+
+
+ +
+
+
diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/remote_cluster_form.js b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/remote_cluster_form.js index fcc7a97a5a38f3..a12dd9c20d030a 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/remote_cluster_form.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/remote_cluster_form.js @@ -38,6 +38,8 @@ import { transportPortUrl, } from '../../../services/documentation'; +import { RequestFlyout } from './request_flyout'; + import { validateName, validateSeeds, validateSeed } from './validators'; const defaultFields = { @@ -78,9 +80,16 @@ export class RemoteClusterForm extends Component { disabledFields, fieldsErrors: this.getFieldsErrors(fieldsState), areErrorsVisible: false, + isRequestVisible: false, }; } + toggleRequest = () => { + this.setState(({ isRequestVisible }) => ({ + isRequestVisible: !isRequestVisible, + })); + }; + getFieldsErrors(fields, seedInput = '') { const { name, seeds } = fields; return { @@ -385,7 +394,7 @@ export class RemoteClusterForm extends Component { renderActions() { const { isSaving, cancel } = this.props; - const { areErrorsVisible } = this.state; + const { areErrorsVisible, isRequestVisible } = this.state; if (isSaving) { return ( @@ -427,25 +436,47 @@ export class RemoteClusterForm extends Component { const isSaveDisabled = areErrorsVisible && this.hasErrors(); return ( - + - - - + + + + + + + + {cancelButton} + - {cancelButton} + + + {isRequestVisible ? ( + + ) : ( + + )} + + ); } @@ -593,6 +624,7 @@ export class RemoteClusterForm extends Component { } = this.props; const { + isRequestVisible, areErrorsVisible, fields: { name, @@ -667,6 +699,14 @@ export class RemoteClusterForm extends Component { {this.renderActions()} {this.renderSavingFeedback()} + + {isRequestVisible ? ( + this.setState({ isRequestVisible: false })} + /> + ) : null} ); } diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/request_flyout.js b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/request_flyout.js new file mode 100644 index 00000000000000..bf315fa79f8566 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/request_flyout.js @@ -0,0 +1,100 @@ +/* + * 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. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { PureComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import PropTypes from 'prop-types'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { serializeCluster } from '../../../../../common'; + +export class RequestFlyout extends PureComponent { + static propTypes = { + close: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + cluster: PropTypes.object.isRequired, + }; + + render() { + const { name, close, cluster } = this.props; + const endpoint = 'PUT _cluster/settings'; + const payload = JSON.stringify(serializeCluster(cluster), null, 2); + const request = `${endpoint}\n${payload}`; + + return ( + + + +

+ {name ? ( + + ) : ( + + )} +

+
+
+ + + +

+ +

+
+ + + + + {request} + +
+ + + + + + +
+ ); + } +} diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts index 87c9e989f2ef84..36b8d4fe7c3a0f 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts @@ -11,8 +11,8 @@ import { RouterRouteHandler, wrapCustomError, } from '../../../../../server/lib/create_router'; +import { serializeCluster } from '../../../common/cluster_serialization'; import { doesClusterExist } from '../../lib/does_cluster_exist'; -import { serializeCluster } from '../../lib/cluster_serialization'; export const register = (router: Router): void => { router.post('', addHandler); diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts index 2d5bf8b1b296fe..eff7c66b265b86 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -13,8 +13,8 @@ import { wrapEsError, wrapUnknownError, } from '../../../../../server/lib/create_router'; +import { serializeCluster } from '../../../common/cluster_serialization'; import { doesClusterExist } from '../../lib/does_cluster_exist'; -import { serializeCluster } from '../../lib/cluster_serialization'; export const register = (router: Router, isEsError: any): void => { router.delete('/{nameOrNames}', createDeleteHandler(isEsError)); diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts index d37f98cdb9e07d..97bb59de85b899 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts @@ -7,7 +7,7 @@ import { get } from 'lodash'; import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router'; -import { deserializeCluster } from '../../lib/cluster_serialization'; +import { deserializeCluster } from '../../../common/cluster_serialization'; export const register = (router: Router): void => { router.get('', getAllHandler); diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts index ab522292eeb152..d6eedf7924ca33 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts @@ -11,8 +11,8 @@ import { RouterRouteHandler, wrapCustomError, } from '../../../../../server/lib/create_router'; +import { serializeCluster, deserializeCluster } from '../../../common/cluster_serialization'; import { doesClusterExist } from '../../lib/does_cluster_exist'; -import { serializeCluster, deserializeCluster } from '../../lib/cluster_serialization'; export const register = (router: Router): void => { router.put('/{name}', updateHandler); diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts index 3c2582475089bb..ddab7eff6835ea 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts @@ -24,6 +24,10 @@ export const unrestrictedFeaturePrivileges = { privileges: ['all'], canUnassign: true, }, + feature4: { + privileges: ['all', 'read'], + canUnassign: true, + }, }, }; @@ -48,5 +52,9 @@ export const fullyRestrictedFeaturePrivileges = { privileges: ['all'], canUnassign: false, }, + feature4: { + privileges: ['all', 'read'], + canUnassign: false, + }, }, }; diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts index d598a9da67a51d..90d3ad388dc7d3 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts @@ -8,7 +8,7 @@ import { KibanaPrivileges } from '../../../../common/model'; export const defaultPrivilegeDefinition = new KibanaPrivileges({ global: { all: ['api:/*', 'ui:/*'], - read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/*'], + read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/*', 'ui:/feature4/foo'], }, space: { all: [ @@ -18,8 +18,9 @@ export const defaultPrivilegeDefinition = new KibanaPrivileges({ 'ui:/feature2/*', 'ui:/feature3/foo', 'ui:/feature3/foo/*', + 'ui:/feature4/foo', ], - read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/bar'], + read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/bar', 'ui:/feature4/foo'], }, features: { feature1: { @@ -33,6 +34,10 @@ export const defaultPrivilegeDefinition = new KibanaPrivileges({ feature3: { all: ['ui:/feature3/foo', 'ui:/feature3/foo/*'], }, + feature4: { + all: ['somethingObscure:/feature4/foo', 'ui:/feature4/foo'], + read: ['ui:/feature4/foo'], + }, }, reserved: {}, }); diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts index ac3c74a987766d..1168a45fa2b23e 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts @@ -135,6 +135,10 @@ describe('AllowedPrivileges', () => { privileges: ['all'], canUnassign: true, // feature 3 has no "read" privilege governed by global "all" }, + feature4: { + privileges: ['all', 'read'], + canUnassign: false, + }, }, }; @@ -198,6 +202,10 @@ describe('AllowedPrivileges', () => { privileges: ['all'], canUnassign: true, // feature 3 has no "read" privilege governed by space "all" }, + feature4: { + privileges: ['all', 'read'], + canUnassign: false, + }, }, }, ]); @@ -236,6 +244,10 @@ describe('AllowedPrivileges', () => { privileges: ['all'], canUnassign: false, }, + feature4: { + privileges: ['all', 'read'], + canUnassign: false, + }, }, }, ]); @@ -250,6 +262,7 @@ describe('AllowedPrivileges', () => { feature: { feature1: ['all'], feature2: ['read'], + feature4: ['all'], }, }, { @@ -289,6 +302,10 @@ describe('AllowedPrivileges', () => { privileges: ['all'], canUnassign: true, // feature 3 has no "read" privilege governed by space "all" }, + feature4: { + privileges: ['all'], + canUnassign: false, + }, }, }, ]); diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts index d21255dc1c5152..416b7404af2daa 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts @@ -63,6 +63,7 @@ interface TestOpts { privilegeIndex?: number; ignoreAssigned?: boolean; result: Record; + feature?: string; } function runTest( @@ -73,6 +74,7 @@ function runTest( privilegeIndex = 0, ignoreAssigned = false, only = false, + feature = 'feature1', }: TestOpts ) { const fn = only ? it.only : it; @@ -91,7 +93,7 @@ function runTest( const actualResult = featurePrivilegeCalculator.getMostPermissiveFeaturePrivilege( role.kibana[privilegeIndex], baseExplanation, - 'feature1', + feature, ignoreAssigned ); @@ -321,6 +323,7 @@ describe('getMostPermissiveFeaturePrivilege', () => { actualPrivilege: 'read', actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true, }, }); @@ -457,6 +460,7 @@ describe('getMostPermissiveFeaturePrivilege', () => { actualPrivilege: 'all', actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true, }, } ); @@ -541,6 +545,7 @@ describe('getMostPermissiveFeaturePrivilege', () => { actualPrivilege: 'all', actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: false, }, }); @@ -572,6 +577,92 @@ describe('getMostPermissiveFeaturePrivilege', () => { supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, }, }); + + describe('feature with "all" excluded from base privileges', () => { + runTest('returns "read" when "all" assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + feature: 'feature4', + privilegeIndex: 1, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "read" when "all" assigned as the global base privilege, which does not override assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature4: ['read'], + }, + }, + ], + }, + feature: 'feature4', + privilegeIndex: 1, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the feature privilege, which is more permissive than the base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature4: ['all'], + }, + }, + ], + }, + feature: 'feature4', + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true, + }, + } + ); + }); }); describe('for space feature privileges, ignoring assigned', () => { diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts index 1cdc70878ecd66..597a05a5372b1c 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts @@ -51,6 +51,8 @@ export class KibanaFeaturePrivilegeCalculator { actualPrivilege: featurePrivilege, actualPrivilegeSource: scenario.actualPrivilegeSource, isDirectlyAssigned: scenario.isDirectlyAssigned, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: + scenario.directlyAssignedFeaturePrivilegeMorePermissiveThanBase, ...this.buildSupercededFields( !scenario.isDirectlyAssigned, scenario.supersededPrivilege, @@ -141,13 +143,19 @@ export class KibanaFeaturePrivilegeCalculator { } if (!ignoreAssigned) { + const actions = this.getFeatureActions( + featureId, + this.getAssignedFeaturePrivilege(privilegeSpec, featureId) + ); + const directlyAssignedFeaturePrivilegeMorePermissiveThanBase = !areActionsFullyCovered( + this.assignedGlobalBaseActions, + actions + ); scenarios.push({ actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, isDirectlyAssigned: true, - actions: this.getFeatureActions( - featureId, - this.getAssignedFeaturePrivilege(privilegeSpec, featureId) - ), + directlyAssignedFeaturePrivilegeMorePermissiveThanBase, + actions, }); } diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts index e6b4f67e6acfc2..70aa88877303ff 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts @@ -29,23 +29,24 @@ const buildEffectivePrivileges = ( return factory.getInstance(role); }; -const buildExpectedFeaturePrivileges = ( - expectedFeaturePrivileges: PrivilegeExplanation | { [featureId: string]: PrivilegeExplanation } -) => { - if (expectedFeaturePrivileges.hasOwnProperty('actualPrivilege')) { - return { - feature: { - feature1: expectedFeaturePrivileges, - feature2: expectedFeaturePrivileges, - feature3: expectedFeaturePrivileges, - }, - }; - } +interface BuildExpectedFeaturePrivilegesOption { + features: string[]; + privilegeExplanation: PrivilegeExplanation; +} +const buildExpectedFeaturePrivileges = (options: BuildExpectedFeaturePrivilegesOption[]) => { return { - feature: { - ...expectedFeaturePrivileges, - }, + feature: options.reduce((acc1, option) => { + return { + ...acc1, + ...option.features.reduce((acc2, featureId) => { + return { + ...acc2, + [featureId]: option.privilegeExplanation, + }; + }, {}), + }; + }, {}), }; }; @@ -78,17 +79,22 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: NO_PRIVILEGE_VALUE, - actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, - isDirectlyAssigned: true, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3', 'feature4'], + privilegeExplanation: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }, + ]), }, ]); }); describe(`with global base privilege of "all"`, () => { - it(`calculates global feature privileges === all`, () => { + it(`calculates global feature privilege of all for features 1-3 and read for feature 4`, () => { const role = buildRole({ spacesPrivileges: [ { @@ -108,16 +114,29 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }, + ]), }, ]); }); - it(`calculates space base and feature privileges === all`, () => { + it(`calculates space base and feature privilege of all for features 1-3 and read for feature 4`, () => { const role = buildRole({ spacesPrivileges: [ { @@ -143,11 +162,24 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: false, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }, + ]), }); }); @@ -162,6 +194,7 @@ describe('calculateEffectivePrivileges', () => { feature1: ['read'], feature2: ['read'], feature3: ['read'], + feature4: ['read'], }, }, { @@ -171,6 +204,7 @@ describe('calculateEffectivePrivileges', () => { feature1: ['read'], feature2: ['read'], feature3: ['read'], + feature4: ['read'], }, }, ], @@ -185,13 +219,26 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, - supersededPrivilege: 'read', - supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + }, + }, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }, + ]), }, { base: { @@ -199,13 +246,27 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: false, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, - supersededPrivilege: 'read', - supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }, + }, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: false, + }, + }, + ]), }, ]); }); @@ -238,23 +299,32 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - feature1: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - feature2: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + { + features: ['feature3'], + privilegeExplanation: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, }, - feature3: { - actualPrivilege: NO_PRIVILEGE_VALUE, - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, - isDirectlyAssigned: true, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - }), + ]), }, { base: { @@ -262,23 +332,32 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: false, }, - ...buildExpectedFeaturePrivileges({ - feature1: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - feature2: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + { + features: ['feature3'], + privilegeExplanation: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, }, - feature3: { - actualPrivilege: NO_PRIVILEGE_VALUE, - actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, - isDirectlyAssigned: true, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - }), + ]), }, ]); }); @@ -294,6 +373,7 @@ describe('calculateEffectivePrivileges', () => { feature1: ['all'], feature2: ['all'], feature3: ['all'], + feature4: ['all'], }, }, { @@ -303,6 +383,7 @@ describe('calculateEffectivePrivileges', () => { feature1: ['all'], feature2: ['all'], feature3: ['all'], + feature4: ['all'], }, }, ], @@ -317,23 +398,16 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - feature1: { - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, - isDirectlyAssigned: true, - }, - feature2: { - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, - isDirectlyAssigned: true, + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3', 'feature4'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, }, - feature3: { - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, - isDirectlyAssigned: true, - }, - }), + ]), }, { base: { @@ -341,11 +415,17 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: false, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, - isDirectlyAssigned: true, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3', 'feature4'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true, + }, + }, + ]), }, ]); }); @@ -378,23 +458,32 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - feature1: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - feature2: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + { + features: ['feature3'], + privilegeExplanation: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, }, - feature3: { - actualPrivilege: NO_PRIVILEGE_VALUE, - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, - isDirectlyAssigned: true, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - }), + ]), }, { base: { @@ -402,16 +491,29 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, - isDirectlyAssigned: false, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + }, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + }, + ]), }, ]); }); - it(`calcualtes "all" for space base and space features when superceded by global "all"`, () => { + it(`calculates "all" for space base and space features when superceded by global "all"`, () => { const role = buildRole({ spacesPrivileges: [ { @@ -436,11 +538,24 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }, + ]), }, { base: { @@ -450,11 +565,24 @@ describe('calculateEffectivePrivileges', () => { supersededPrivilege: 'read', supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, }, - ...buildExpectedFeaturePrivileges({ - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, - }), + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + }, + ]), }, ]); }); @@ -474,6 +602,7 @@ describe('calculateEffectivePrivileges', () => { feature1: ['all'], feature2: ['all'], feature3: ['all'], + feature4: ['all'], }, }, ], @@ -488,23 +617,32 @@ describe('calculateEffectivePrivileges', () => { actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, isDirectlyAssigned: true, }, - ...buildExpectedFeaturePrivileges({ - feature1: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - feature2: { - actualPrivilege: 'read', - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, - isDirectlyAssigned: false, + { + features: ['feature3'], + privilegeExplanation: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, }, - feature3: { - actualPrivilege: NO_PRIVILEGE_VALUE, - actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, - isDirectlyAssigned: true, + { + features: ['feature4'], + privilegeExplanation: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, }, - }), + ]), }, { base: { @@ -514,23 +652,17 @@ describe('calculateEffectivePrivileges', () => { supersededPrivilege: 'read', supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, }, - ...buildExpectedFeaturePrivileges({ - feature1: { - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, - isDirectlyAssigned: true, - }, - feature2: { - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, - isDirectlyAssigned: true, - }, - feature3: { - actualPrivilege: 'all', - actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, - isDirectlyAssigned: true, + ...buildExpectedFeaturePrivileges([ + { + features: ['feature1', 'feature2', 'feature3', 'feature4'], + privilegeExplanation: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true, + }, }, - }), + ]), }, ]); }); @@ -636,6 +768,10 @@ describe('calculateAllowedPrivileges', () => { privileges: ['all'], canUnassign: true, // feature 3 has no "read" privilege governed by global "all" }, + feature4: { + privileges: ['all', 'read'], + canUnassign: false, + }, }, }; @@ -697,6 +833,10 @@ describe('calculateAllowedPrivileges', () => { privileges: ['all'], canUnassign: true, // feature 3 has no "read" privilege governed by space "all" }, + feature4: { + privileges: ['all', 'read'], + canUnassign: false, + }, }, }, ]); @@ -733,6 +873,10 @@ describe('calculateAllowedPrivileges', () => { privileges: ['all'], canUnassign: false, }, + feature4: { + privileges: ['all', 'read'], + canUnassign: false, + }, }, }, ]); @@ -747,6 +891,7 @@ describe('calculateAllowedPrivileges', () => { feature: { feature1: ['all'], feature2: ['read'], + feature4: ['all'], }, }, { @@ -784,6 +929,10 @@ describe('calculateAllowedPrivileges', () => { privileges: ['all'], canUnassign: true, // feature 3 has no "read" privilege governed by space "all" }, + feature4: { + privileges: ['all'], + canUnassign: false, + }, }, }, ]); diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts index 41f6012737a920..aeaf12d02210a0 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts @@ -27,6 +27,7 @@ export interface PrivilegeExplanation { isDirectlyAssigned: boolean; supersededPrivilege?: string; supersededPrivilegeSource?: PRIVILEGE_SOURCE; + directlyAssignedFeaturePrivilegeMorePermissiveThanBase?: boolean; } export interface CalculatedPrivilege { @@ -43,6 +44,7 @@ export interface PrivilegeScenario { supersededPrivilege?: string; supersededPrivilegeSource?: PRIVILEGE_SOURCE; actions: string[]; + directlyAssignedFeaturePrivilegeMorePermissiveThanBase?: boolean; } export interface AllowedPrivilege { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts index 54f9c33aafe281..d412ba63403e19 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts @@ -8,12 +8,17 @@ import { RawKibanaPrivileges } from '../../../../../../../../../common/model'; export const rawKibanaPrivileges: RawKibanaPrivileges = { global: { - all: ['normal-feature-all', 'normal-feature-read', 'just-global-all'], - read: ['normal-feature-read'], + all: [ + 'normal-feature-all', + 'normal-feature-read', + 'just-global-all', + 'all-privilege-excluded-from-base-read', + ], + read: ['normal-feature-read', 'all-privilege-excluded-from-base-read'], }, space: { - all: ['normal-feature-all', 'normal-feature-read'], - read: ['normal-feature-read'], + all: ['normal-feature-all', 'normal-feature-read', 'all-privilege-excluded-from-base-read'], + read: ['normal-feature-read', 'all-privilege-excluded-from-base-read'], }, reserved: {}, features: { @@ -21,9 +26,13 @@ export const rawKibanaPrivileges: RawKibanaPrivileges = { all: ['normal-feature-all', 'normal-feature-read'], read: ['normal-feature-read'], }, - excludedFromBase: { - all: ['excluded-from-base-all', 'excluded-from-base-read'], - read: ['excluded-from-base-read'], + bothPrivilegesExcludedFromBase: { + all: ['both-privileges-excluded-from-base-all', 'both-privileges-excluded-from-base-read'], + read: ['both-privileges-excluded-from-base-read'], + }, + allPrivilegeExcludedFromBase: { + all: ['all-privilege-excluded-from-base-all', 'all-privilege-excluded-from-base-read'], + read: ['all-privilege-excluded-from-base-read'], }, }, }; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index a11ad5cba226e0..36a7c69d5b738d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -294,7 +294,14 @@ exports[` renders without crashing 1`] = ` ], }, "feature": Object { - "excludedFromBase": Object { + "allPrivilegeExcludedFromBase": Object { + "canUnassign": true, + "privileges": Array [ + "all", + "read", + ], + }, + "bothPrivilegesExcludedFromBase": Object { "canUnassign": true, "privileges": Array [ "all", @@ -319,7 +326,12 @@ exports[` renders without crashing 1`] = ` "isDirectlyAssigned": true, }, "feature": Object { - "excludedFromBase": Object { + "allPrivilegeExcludedFromBase": Object { + "actualPrivilege": "none", + "actualPrivilegeSource": 30, + "isDirectlyAssigned": true, + }, + "bothPrivilegesExcludedFromBase": Object { "actualPrivilege": "none", "actualPrivilegeSource": 30, "isDirectlyAssigned": true, @@ -441,13 +453,22 @@ exports[` renders without crashing 1`] = ` KibanaPrivileges { "rawKibanaPrivileges": Object { "features": Object { - "excludedFromBase": Object { + "allPrivilegeExcludedFromBase": Object { "all": Array [ - "excluded-from-base-all", - "excluded-from-base-read", + "all-privilege-excluded-from-base-all", + "all-privilege-excluded-from-base-read", ], "read": Array [ - "excluded-from-base-read", + "all-privilege-excluded-from-base-read", + ], + }, + "bothPrivilegesExcludedFromBase": Object { + "all": Array [ + "both-privileges-excluded-from-base-all", + "both-privileges-excluded-from-base-read", + ], + "read": Array [ + "both-privileges-excluded-from-base-read", ], }, "normal": Object { @@ -465,9 +486,11 @@ exports[` renders without crashing 1`] = ` "normal-feature-all", "normal-feature-read", "just-global-all", + "all-privilege-excluded-from-base-read", ], "read": Array [ "normal-feature-read", + "all-privilege-excluded-from-base-read", ], }, "reserved": Object {}, @@ -475,9 +498,11 @@ exports[` renders without crashing 1`] = ` "all": Array [ "normal-feature-all", "normal-feature-read", + "all-privilege-excluded-from-base-read", ], "read": Array [ "normal-feature-read", + "all-privilege-excluded-from-base-read", ], }, }, @@ -487,7 +512,11 @@ exports[` renders without crashing 1`] = ` onChangeAll={[Function]} rankedFeaturePrivileges={ Object { - "excludedFromBase": Array [ + "allPrivilegeExcludedFromBase": Array [ + "all", + "read", + ], + "bothPrivilegesExcludedFromBase": Array [ "all", "read", ], diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 7a736878425409..1025a340b2d86c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -120,9 +120,9 @@ describe('only global', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -131,9 +131,31 @@ describe('only global', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('bothPrivilegesExcludedFromBase feature privilege read', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -184,9 +206,13 @@ describe('only default and marketing space', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { const props = buildProps([ - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -195,9 +221,43 @@ describe('only default and marketing space', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('bothPrivilegesExcludedFromBase feature privilege read', () => { const props = buildProps([ - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -261,10 +321,14 @@ describe('global base all', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { const props = buildProps([ { spaces: ['*'], base: ['all'], feature: {} }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -274,10 +338,14 @@ describe('global base all', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('bothPrivilegesExcludedFromBase feature privilege read', () => { const props = buildProps([ { spaces: ['*'], base: ['all'], feature: {} }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -286,6 +354,40 @@ describe('global base all', () => { { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, ]); }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: true } }, + ]); + }); }); }); @@ -326,7 +428,7 @@ describe('global base read', () => { const actualTable = getTableFromComponent(component); expect(actualTable).toEqual([ { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, - { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: true } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, ]); }); @@ -343,10 +445,31 @@ describe('global base read', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('bothPrivilegesExcludedFromBase feature privilege read', () => { const props = buildProps([ { spaces: ['*'], base: ['read'], feature: {} }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -356,10 +479,14 @@ describe('global base read', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('allPrivilegeExcludedFromBase feature privilege all', () => { const props = buildProps([ { spaces: ['*'], base: ['read'], feature: {} }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -368,6 +495,23 @@ describe('global base read', () => { { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, ]); }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: true } }, + ]); + }); }); }); @@ -425,10 +569,14 @@ describe('global normal feature privilege all', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { const props = buildProps([ { spaces: ['*'], base: [], feature: { normal: ['all'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -438,10 +586,48 @@ describe('global normal feature privilege all', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('bothPrivilegesExcludedFromBase feature privilege read', () => { const props = buildProps([ { spaces: ['*'], base: [], feature: { normal: ['all'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -507,10 +693,31 @@ describe('global normal feature privilege read', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('bothPrivilegesExcludedFromBase feature privilege read', () => { const props = buildProps([ { spaces: ['*'], base: [], feature: { normal: ['read'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -520,10 +727,279 @@ describe('global normal feature privilege read', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('allPrivilegeExcludedFromBase feature privilege all', () => { const props = buildProps([ { spaces: ['*'], base: [], feature: { normal: ['read'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); + +describe('global bothPrivilegesExcludedFromBase feature privilege all', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('bothPrivilegesExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('bothPrivilegesExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); + +describe('global bothPrivilegesExcludedFromBase feature privilege read', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('bothPrivilegesExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('bothPrivilegesExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { bothPrivilegesExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -535,11 +1011,11 @@ describe('global normal feature privilege read', () => { }); }); -describe('global excludedFromBase feature privilege all', () => { +describe('global allPrivilegeExcludedFromBase feature privilege all', () => { describe('default and marketing space', () => { it('base all', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, ]); const component = mountWithIntl(); @@ -552,7 +1028,7 @@ describe('global excludedFromBase feature privilege all', () => { it('base read', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, ]); const component = mountWithIntl(); @@ -565,7 +1041,7 @@ describe('global excludedFromBase feature privilege all', () => { it('normal feature privilege all', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, ]); const component = mountWithIntl(); @@ -578,7 +1054,7 @@ describe('global excludedFromBase feature privilege all', () => { it('normal feature privilege read', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, ]); const component = mountWithIntl(); @@ -589,10 +1065,14 @@ describe('global excludedFromBase feature privilege all', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -602,10 +1082,48 @@ describe('global excludedFromBase feature privilege all', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('bothPrivilegesExcludedFromBase feature privilege read', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['all'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -617,11 +1135,11 @@ describe('global excludedFromBase feature privilege all', () => { }); }); -describe('global excludedFromBase feature privilege read', () => { +describe('global allPrivilegeExcludedFromBase feature privilege read', () => { describe('default and marketing space', () => { it('base all', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, ]); const component = mountWithIntl(); @@ -634,7 +1152,7 @@ describe('global excludedFromBase feature privilege read', () => { it('base read', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, ]); const component = mountWithIntl(); @@ -647,7 +1165,7 @@ describe('global excludedFromBase feature privilege read', () => { it('normal feature privilege all', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, ]); const component = mountWithIntl(); @@ -660,7 +1178,7 @@ describe('global excludedFromBase feature privilege read', () => { it('normal feature privilege read', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, ]); const component = mountWithIntl(); @@ -671,10 +1189,48 @@ describe('global excludedFromBase feature privilege read', () => { ]); }); - it('excludedFromBase feature privilege all', () => { + it('bothPrivilegesExcludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['all'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('bothPrivilegesExcludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { bothPrivilegesExcludedFromBase: ['read'] }, + }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('allPrivilegeExcludedFromBase feature privilege all', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['all'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); @@ -684,10 +1240,14 @@ describe('global excludedFromBase feature privilege read', () => { ]); }); - it('excludedFromBase feature privilege read', () => { + it('allPrivilegeExcludedFromBase feature privilege read', () => { const props = buildProps([ - { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, - { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['*'], base: [], feature: { allPrivilegeExcludedFromBase: ['read'] } }, + { + spaces: ['default', 'marketing'], + base: [], + feature: { allPrivilegeExcludedFromBase: ['read'] }, + }, ]); const component = mountWithIntl(); const actualTable = getTableFromComponent(component); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index f1157203059e51..5a718a34b90057 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -81,8 +81,6 @@ export class PrivilegeSpaceTable extends Component { const effectivePrivileges = privilegeCalculator.calculateEffectivePrivileges(false); - const allowedPrivileges = privilegeCalculator.calculateAllowedPrivileges(); - const rows: TableRow[] = spacePrivileges.map((spacePrivs, spacesIndex) => { const spaces = spacePrivs.spaces.map( spaceId => @@ -199,16 +197,12 @@ export class PrivilegeSpaceTable extends Component { /> ); } else { - const hasNonSupersededCustomizations = Object.entries(privileges.feature).some( - ([featureId, featurePrivileges]) => { - const allowedFeaturePrivileges = - allowedPrivileges[record.spacesIndex].feature[featureId]; + const hasNonSupersededCustomizations = Object.keys(privileges.feature).some( + featureId => { + const featureEffectivePrivilege = effectivePrivilege.feature[featureId]; return ( - allowedFeaturePrivileges && - allowedFeaturePrivileges.canUnassign && - allowedFeaturePrivileges.privileges.some(privilege => - featurePrivileges.includes(privilege) - ) + featureEffectivePrivilege && + featureEffectivePrivilege.directlyAssignedFeaturePrivilegeMorePermissiveThanBase ); } ); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts index 3243606c5721c6..b6b2b56c9a2e49 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts +++ b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts @@ -638,7 +638,7 @@ describe('features', () => { expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); }); - test('actions defined in a feature privilege with excludeFromBasePrivileges are not included in `all` or `read', () => { + test('actions defined in a feature with excludeFromBasePrivileges are not included in `all` or `read', () => { const features: Feature[] = [ { id: 'foo', @@ -711,6 +711,82 @@ describe('features', () => { ]); expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); }); + + test('actions defined in an individual feature privilege with excludeFromBasePrivileges are not included in `all` or `read`', () => { + const features: Feature[] = [ + { + id: 'foo', + name: 'Foo Feature', + icon: 'arrowDown', + navLinkId: 'kibana:foo', + app: [], + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], + }, + privileges: { + bar: { + excludeFromBasePrivileges: true, + management: { + 'bar-management': ['bar-management-1'], + }, + catalogue: ['bar-catalogue-1'], + savedObject: { + all: ['bar-savedObject-all-1'], + read: ['bar-savedObject-read-1'], + }, + ui: ['bar-ui-1'], + }, + all: { + excludeFromBasePrivileges: true, + management: { + 'all-management': ['all-management-1'], + }, + catalogue: ['all-catalogue-1'], + savedObject: { + all: ['all-savedObject-all-1'], + read: ['all-savedObject-read-1'], + }, + ui: ['all-ui-1'], + }, + read: { + excludeFromBasePrivileges: true, + management: { + 'read-management': ['read-management-1'], + }, + catalogue: ['read-catalogue-1'], + savedObject: { + all: ['read-savedObject-all-1'], + read: ['read-savedObject-read-1'], + }, + ui: ['read-ui-1'], + }, + }, + }, + ]; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(features), + }; + + const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.all`, [ + actions.login, + actions.version, + ...(expectGetFeatures ? [actions.api.get('features')] : []), + ...(expectManageSpaces + ? [ + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + ] + : []), + actions.allHack, + ]); + expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); + }); }); }); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts index aa18a7d150b7e4..8d28b144f6c1fa 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts +++ b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts @@ -27,6 +27,10 @@ export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPl flatten( basePrivilegeFeatures.map(feature => Object.values(feature.privileges).reduce((acc, privilege) => { + if (privilege.excludeFromBasePrivileges) { + return acc; + } + return [...acc, ...featurePrivilegeBuilder.getActions(privilege, feature)]; }, []) ) @@ -37,7 +41,7 @@ export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPl flatten( basePrivilegeFeatures.map(feature => Object.entries(feature.privileges).reduce((acc, [privilegeId, privilege]) => { - if (privilegeId !== 'read') { + if (privilegeId !== 'read' || privilege.excludeFromBasePrivileges) { return acc; } diff --git a/x-pack/legacy/plugins/siem/cypress.json b/x-pack/legacy/plugins/siem/cypress.json index e5f7bdd0f78755..ae73965f8f10f9 100644 --- a/x-pack/legacy/plugins/siem/cypress.json +++ b/x-pack/legacy/plugins/siem/cypress.json @@ -1,3 +1,7 @@ { - "baseUrl": "http://localhost:5601" + "baseUrl": "http://localhost:5601", + "screenshotsFolder": "../../../../target/kibana-siem/cypress/screenshots", + "trashAssetsBeforeRuns": false, + "video": false, + "videosFolder": "../../../../target/kibana-siem/cypress/videos" } diff --git a/x-pack/legacy/plugins/siem/cypress/README.md b/x-pack/legacy/plugins/siem/cypress/README.md index 257c4f348c4955..996d23ab5fab28 100644 --- a/x-pack/legacy/plugins/siem/cypress/README.md +++ b/x-pack/legacy/plugins/siem/cypress/README.md @@ -1,70 +1,242 @@ # Cypress Tests -The `siem/cypress` directory contains end to end tests (specific to the `SIEM` app) that execute via [Cypress](https://www.cypress.io/). +The `siem/cypress` directory contains end to end tests, (plus a few tests +that rely on mocked API calls), that execute via [Cypress](https://www.cypress.io/). -At present, these tests are only executed in a local development environment; they are **not** integrated in the Kibana CI infrastructure, and therefore do **not** run automatically when you submit a PR. +Cypress tests may be run against: -See the `Server and Authentication Requirements` section below for additional details. +- A local Kibana instance, interactively or via the command line. Credentials +are specified via `kibna.dev.yml` or environment variables. +- A remote Elastic Cloud instance (override `baseUrl`), interactively or via +the command line. Again, credentials are specified via `kibna.dev.yml` or +environment variables. +- As part of CI (override `baseUrl` and pass credentials via the +`CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables), via command line. -## Organizing Tests and (Mock) Data +At present, Cypress tests are only executed manually. They are **not** yet +integrated in the Kibana CI infrastructure, and therefore do **not** run +automatically when you submit a PR. -- Code and CSS selectors that may be re-used across tests should be added to `siem/cypress/integration/lib`, as described below -- Smoke Tests are located in `siem/cypress/integration/smoke_tests` -- Mocked responses from the server are located in `siem/cypress/fixtures` +## Smoke Tests -### `cypress/integration/lib` +Smoke Tests are located in `siem/cypress/integration/smoke_tests` -The `cypress/integration/lib` folder contains code intended to be re-used across many different tests. +## Test Helpers -- Files named `helpers.ts` (e.g. `siem/cypress/integration/lib/login/helpers.ts`) contain functions (e.g. `login`) that may be imported and invoked from multiple tests. +_Test helpers_ are functions that may be re-used across tests. -- Files named `selectors.ts` export CSS selectors for re-use. For example, `siem/cypress/integration/lib/login/selectors.ts` exports the following selector that matches the Username text area in the Kibana login page: +- Reusable code and CSS selectors should be added to +`siem/cypress/integration/lib`, as described below. -``` +### Reusable Test Helper Functions and CSS Selectors + +The `cypress/integration/lib` directory contains code intended to be re-used +across many different tests. Add reusable test helper functions and CSS +selectors to directories under `cypress/integration/lib`. + +- Files named `helpers.ts` (e.g. `siem/cypress/integration/lib/login/helpers.ts`) +contain functions (e.g. `login`) that may be imported and invoked from multiple tests. + +- Files named `selectors.ts` export CSS selectors for re-use. For example, +`siem/cypress/integration/lib/login/selectors.ts` exports the following selector +that matches the Username text area in the Kibana login page: + +```sh export const USERNAME = '[data-test-subj="loginUsername"]'; ``` -## Server and Authentication Requirements +## Mock Data + +We prefer not to mock API responses in most of our smoke tests, but sometimes +it's necessary because a test must assert that a specific value is rendered, +and it's not possible to derive that value based on the data in the +envrionment where tests are running. + +Mocked responses API from the server are located in `siem/cypress/fixtures`. + +## Authentication + +When running tests, there are two ways to specify the credentials used to +authenticate with Kibana: + +- Via `kibana.dev.yml` (recommended for developers) +- Via the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables (recommended for CI), or when testing a remote Kibana +instance, e.g. in Elastic Cloud. + +Note: Tests that use the `login()` test helper function for authentication will +automatically use the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables when they are defined, and fall back to the values in +`config/kibana.dev.yml` when they are unset. + +### Content Security Policy (CSP) Settings + +Your local or cloud Kibana server must have the `csp.strict: false` setting +configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below: + +```yaml +csp.strict: false +``` + +The above setting is required to prevent the _Please upgrade +your browser_ / _This Kibana installation has strict security requirements +enabled that your current browser does not meet._ warning that's displayed for +unsupported user agents, like the one reported by Cypress when running tests. + +### Example `kibana.dev.yml` -The current version of the Smoke Tests require running a local Kibana server that connects to an instance of `elasticsearch`. A file named `config/kibana.dev.yml` like the example below is required to run the tests: +If you're a developer running tests interactively or on the command line, the +easiset way to specify the credentials used for authentication is to update + `kibana.dev.yml` per the following example: ```yaml +csp.strict: false elasticsearch: username: 'elastic' password: '' hosts: ['https://:9200'] ``` -The `username` and `password` from `config/kibana.dev.yml` will be read by the `login` test helper function when tests authenticate with Kibana. +## Running Tests Interactively -See the `Running Tests Interactively` section for details. +Use the Cypress interactive test runner to develop and debug specific tests +by adding a `.only` to the test you're developing, or click on a specific +spec in the interactive test runner to run just the tests in that spec. -### Content Security Policy (CSP) Settings +To run and debug tests in interactively via the Cypress test runner: - Your local or cloud Kibana server must have the `csp.strict: false` setting -configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below: +1. Disable CSP on the local or remote Kibana instance, as described in the +_Content Security Policy (CSP) Settings_ section above. - ```yaml -csp.strict: false +2. To specify the credentials required for authentication, configure +`config/kibana.dev.yml`, as described in the _Server and Authentication +Requirements_ section above, or specify them via environment variables +as described later in this section. + +3. Start a local instance of the Kibana development server (only if testing against a +local host): + +```sh +yarn start --no-base-path ``` -## Running Tests Interactively +4. Launch the Cypress interactive test runner via one of the following options: -To run tests in interactively via the Cypress test runner: +- To run tests interactively against the default (local) host specified by +`baseUrl`, as configured in `plugins/siem/cypress.json`: -1. Create and configure a `config/kibana.dev.yml`, as described in the `Server and Authentication Requirements` section above. +```sh +cd x-pack/legacy/plugins/siem +yarn cypress:open +``` + +- To (optionally) run tests interactively against a different host, pass the +`CYPRESS_baseUrl` environment variable on the command line when launching the +test runner, as shown in the following example: + +```sh +cd x-pack/legacy/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 yarn cypress:open +``` -2. Start a local instance of the Kibana development server: +- To (optionally) override username and password via environment variables when +running tests interactively: +```sh +cd x-pack/legacy/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:open ``` + +5. Click the `Run all specs` button in the Cypress test runner (after adding +a `.only` to an `it` or `describe` block). + +## Running (Headless) Tests on the Command Line + +To run (headless) tests on the command line: + +1. Disable CSP on the local or remote Kibana instance, as described in the +_Content Security Policy (CSP) Settings_ section above. + +2. To specify the credentials required for authentication, configure +`config/kibana.dev.yml`, as described in the _Server and Authentication +Requirements_ section above, or specify them via environment variables +as described later in this section. + +3. Start a local instance of the Kibana development server (only if testing against a +local host): + +```sh yarn start --no-base-path ``` -3. Launch the Cypress interactive test runner: +4. Launch the Cypress command line test runner via one of the following options: + +- To run tests on the command line against the default (local) host specified by +`baseUrl`, as configured in `plugins/siem/cypress.json`: ```sh cd x-pack/legacy/plugins/siem -yarn cypress:open +yarn cypress:run +``` + +- To (optionally) run tests on the command line against a different host, pass +`CYPRESS_baseUrl` as an environment variable on the command line, as shown in +the following example: + +```sh +cd x-pack/legacy/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 yarn cypress:run ``` -4. Click the `Run all specs` button in the Cypress test runner +- To (optionally) override username and password via environment variables when +running via the command line: + +```sh +cd x-pack/legacy/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run +``` + +## Reporting + +When Cypress tests are run on the command line via `yarn cypress:run`, +reporting artifacts are generated under the `target` directory in the root +of the Kibana, as detailed for each artifact type in the sections bleow. + +### HTML Reports + +An HTML report (e.g. for email notifications) is output to: + +``` +target/kibana-siem/cypress/results/output.html +``` + +### Screenshots + +Screenshots of failed tests are output to: + +``` +target/kibana-siem/cypress/screenshots +``` + +### `junit` Reports + +The Kibana CI process reports `junit` test results from the `target/junit` directory. + +Cypress `junit` reports are generated in `target/kibana-siem/cypress/results` +and copied to the `target/junit` directory. + +### Videos (optional) + +Videos are disabled by default, but can optionally be enabled by setting the +`CYPRESS_video=true` environment variable: + +``` +CYPRESS_video=true yarn cypress:run +``` + +Videos are (optionally) output to: + +``` +target/kibana-siem/cypress/videos +``` diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts index 546438284b947f..b3739ee838a432 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts @@ -21,7 +21,7 @@ import { } from '../../lib/fields_browser/selectors'; import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; -import { loginAndWaitForPage } from '../../lib/util/helpers'; +import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; const defaultHeaders = [ { id: '@timestamp' }, @@ -200,9 +200,9 @@ describe('Fields Browser', () => { cy.get(`[data-test-subj="headers-group"]`).then(headersDropArea => drop(headersDropArea)); - clickOutsideFieldsBrowser(); - - cy.get(`[data-test-subj="header-text-${toggleField}"]`).should('exist'); + cy.get(`[data-test-subj="header-text-${toggleField}"]`, { timeout: DEFAULT_TIMEOUT }).should( + 'exist' + ); }); it('resets all fields in the timeline when `Reset Fields` is clicked', () => { diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 828b3cd9cd4104..c10d0d7cee22b6 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -9,7 +9,7 @@ import { populateTimeline } from '../../lib/fields_browser/helpers'; import { logout } from '../../lib/logout'; import { toggleFirstEventDetails } from '../../lib/timeline/helpers'; import { HOSTS_PAGE } from '../../lib/urls'; -import { loginAndWaitForPage } from '../../lib/util/helpers'; +import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; describe('toggle column in timeline', () => { beforeEach(() => { @@ -72,6 +72,8 @@ describe('toggle column in timeline', () => { cy.get(`[data-test-subj="headers-group"]`).then(headersDropArea => drop(headersDropArea)); - cy.get(`[data-test-subj="header-text-${idField}"]`).should('exist'); + cy.get(`[data-test-subj="header-text-${idField}"]`, { timeout: DEFAULT_TIMEOUT }).should( + 'exist' + ); }); }); diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index 30ff0da5f97942..cc9fd177079184 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -6,20 +6,37 @@ "license": "Elastic-License", "scripts": { "build-graphql-types": "node scripts/generate_types_from_graphql.js", - "cypress:open": "cypress open" + "cypress:open": "cypress open", + "cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --reporter mocha-multi-reporters --reporter-options configFile=./reporter_config.json; mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/" }, "devDependencies": { "@cypress/webpack-preprocessor": "^4.1.0", "@types/js-yaml": "^3.12.1", "@types/lodash": "^4.14.110", "@types/react-beautiful-dnd": "^10.0.1", - "cypress": "^3.3.1", + "cypress": "^3.4.1", "js-yaml": "^3.13.1", + "mocha-junit-reporter": "^1.23.1", + "mocha-multi-reporters": "^1.1.7", + "mochawesome": "^4.0.1", + "mochawesome-merge": "^2.0.1", + "mochawesome-report-generator": "^4.0.1", "ts-loader": "^6.0.4" }, "dependencies": { "lodash": "^4.17.13", "react-beautiful-dnd": "^10.0.1", "react-markdown": "^4.0.6" + }, + "workspaces": { + "packages": [ + "packages/*" + ], + "nohoist": [ + "**/mochawesome", + "**/mochawesome/**", + "**/mocha-multi-reporters", + "**/mocha-multi-reporters/**" + ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/reporter_config.json b/x-pack/legacy/plugins/siem/reporter_config.json new file mode 100644 index 00000000000000..ff19c242ad8dc8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/reporter_config.json @@ -0,0 +1,10 @@ +{ + "reporterEnabled": "mochawesome, mocha-junit-reporter", + "reporterOptions": { + "html": false, + "json": true, + "mochaFile": "../../../../target/kibana-siem/cypress/results/results-[hash].xml", + "overwrite": false, + "reportDir": "../../../../target/kibana-siem/cypress/results" + } +} diff --git a/x-pack/legacy/plugins/telemetry/index.ts b/x-pack/legacy/plugins/telemetry/index.ts index 6d4e9be67fb98a..71e80cb2bd02f2 100644 --- a/x-pack/legacy/plugins/telemetry/index.ts +++ b/x-pack/legacy/plugins/telemetry/index.ts @@ -65,12 +65,9 @@ export const telemetry = (kibana: any) => { injectDefaultVars(server: Server) { const config = server.config(); return { - telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'), - spacesEnabled: config.get('xpack.spaces.enabled'), telemetryBanner: config.get('xpack.telemetry.banner'), telemetryOptedIn: null, - activeSpace: null, }; }, hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap index c0eecdaad26c01..2927e867dec1f4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`IntegrationLink component renders a disabled link when href is undefined 1`] = ` - + @@ -32,7 +34,9 @@ exports[`IntegrationLink component renders without errors 1`] = ` href="/app/foo?kuery=localhost" type="button" > - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx index 189eb8a34a0c0c..63a3053afc6016 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx @@ -24,7 +24,7 @@ export const IntegrationLink = ({ tooltipContent, }: IntegrationLinkProps) => typeof href === 'undefined' ? ( - + ) : ( - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index d83117dd538b6c..a8aa1963190e41 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -21,14 +21,6 @@ exports[`MonitorList component renders a no items message when no data is provid { { return { @@ -133,38 +134,6 @@ export const MonitorListComponent = (props: Props) => { // TODO: reintegrate sorting in future release // sorting={sorting} columns={[ - { - align: 'left', - field: 'monitor_id', - name: '', - sortable: true, - width: '40px', - render: (id: string) => { - return ( - item === id) ? 'arrowUp' : 'arrowDown'} - onClick={() => { - if (drawerIds.find(i => id === i)) { - updateDrawerIds(drawerIds.filter(p => p !== id)); - } else { - updateDrawerIds([...drawerIds, id]); - } - }} - /> - ); - }, - }, { align: 'left', field: 'state.monitor.status', @@ -212,6 +181,9 @@ export const MonitorListComponent = (props: Props) => { name: i18n.translate('xpack.uptime.monitorList.monitorHistoryColumnLabel', { defaultMessage: 'Downtime history', }), + mobileOptions: { + show: false, + }, render: (histogramSeries: SummaryHistogramPoint[] | null) => ( { ), }, { + id: 'actions', align: 'right', field: 'state', + hasActions: true, + mobileOptions: { + header: false, + }, name: i18n.translate( 'xpack.uptime.monitorList.observabilityIntegrationsColumnLabel', { @@ -236,6 +213,39 @@ export const MonitorListComponent = (props: Props) => { ), }, + { + align: 'left', + field: 'monitor_id', + name: '', + sortable: true, + width: '40px', + isExpander: true, + render: (id: string) => { + return ( + item === id) ? 'arrowUp' : 'arrowDown'} + onClick={() => { + if (drawerIds.find(i => id === i)) { + updateDrawerIds(drawerIds.filter(p => p !== id)); + } else { + updateDrawerIds([...drawerIds, id]); + } + }} + /> + ); + }, + }, ]} /> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_link.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_link.tsx index 9e6c11c7ae9417..4bfa2f95c3f770 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_link.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_link.tsx @@ -24,9 +24,10 @@ export const MonitorPageLink: FunctionComponent = ({ {children} diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 316c91ee4a9737..bc294dd54d2ed7 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -27,9 +27,8 @@ import { BaseLocationOptions } from '../components/functional/ping_list'; import { useTrackPageview } from '../../../infra/public'; interface MonitorPageProps { - location: { pathname: string; search: string }; logMonitorPageLoad: () => void; - match: { params: { id: string } }; + match: { params: { monitorId: string } }; // this is the query function provided by Apollo's Client API query: ( options: QueryOptions @@ -38,13 +37,13 @@ interface MonitorPageProps { } export const MonitorPage = ({ - location, logMonitorPageLoad, query, setBreadcrumbs, + match, }: MonitorPageProps) => { - const parsedPath = location.pathname.replace(/^(\/monitor\/)/, '').split('/'); - const [monitorId] = useState(decodeURI(parsedPath[0])); + // decode 64 base string, it was decoded to make it a valid url, since monitor id can be a url + const monitorId = atob(match.params.monitorId); const [pingListPageCount, setPingListPageCount] = useState(10); const { colors, refreshApp, setHeadingText } = useContext(UptimeSettingsContext); const [getUrlParams, updateUrlParams] = useUrlParams(); diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index 188951717dd5a1..582eda2acf4adc 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -173,7 +173,7 @@ const Application = (props: UptimeAppProps) => { )} /> ( { 'plugins/xpack_main/hacks/check_xpack_info_change', ], replaceInjectedVars, + injectDefaultVars(server) { + const config = server.config(); + + return { + telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), + activeSpace: null, + spacesEnabled: config.get('xpack.spaces.enabled'), + }; + }, __webpackPluginProvider__(webpack) { return new webpack.BannerPlugin({ banner: dedent` diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts b/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts index 95f2ee7b3d3848..7ca72b610ec971 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts +++ b/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts @@ -11,6 +11,11 @@ import { UICapabilities } from 'ui/capabilities'; * Feature privilege definition */ export interface FeatureKibanaPrivileges { + /** + * Whether or not this specific privilege should be excluded from the base privileges. + */ + excludeFromBasePrivileges?: boolean; + /** * If this feature includes management sections, you can specify them here to control visibility of those * pages based on user privileges. @@ -235,6 +240,7 @@ const managementSchema = Joi.object().pattern( const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); const privilegeSchema = Joi.object({ + excludeFromBasePrivileges: Joi.boolean(), management: managementSchema, catalogue: catalogueSchema, api: Joi.array().items(Joi.string()), diff --git a/x-pack/package.json b/x-pack/package.json index 98eeeedf8590ae..2e47b22d726866 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -119,6 +119,7 @@ "cheerio": "0.22.0", "commander": "3.0.0", "copy-webpack-plugin": "^5.0.0", + "cypress": "^3.4.1", "del": "^4.0.0", "dotenv": "2.0.0", "enzyme": "^3.10.0", @@ -140,8 +141,14 @@ "jest-cli": "^24.8.0", "jest-styled-components": "^6.2.2", "jsdom": "^12.0.0", + "js-yaml": "^3.13.1", "madge": "3.4.4", "mocha": "3.5.3", + "mocha-junit-reporter": "^1.23.1", + "mocha-multi-reporters": "^1.1.7", + "mochawesome": "^4.0.1", + "mochawesome-merge": "^2.0.1", + "mochawesome-report-generator": "^4.0.1", "mustache": "^2.3.0", "mutation-observer": "^1.0.3", "node-fetch": "^2.1.2", @@ -165,6 +172,7 @@ "supertest-as-promised": "^4.0.2", "tmp": "0.1.0", "tree-kill": "^1.1.0", + "ts-loader": "^6.0.4", "typescript": "3.5.3", "vinyl-fs": "^3.0.2", "xml-crypto": "^0.10.1", @@ -175,7 +183,7 @@ "@babel/polyfill": "7.4.4", "@babel/register": "7.4.4", "@babel/runtime": "7.4.5", - "@elastic/ctags-langserver": "^0.1.3", + "@elastic/ctags-langserver": "^0.1.5", "@elastic/datemath": "5.0.2", "@elastic/eui": "13.1.1", "@elastic/javascript-typescript-langserver": "^0.2.2", @@ -360,5 +368,13 @@ }, "engines": { "yarn": "^1.10.1" + }, + "workspaces": { + "nohoist": [ + "**/mochawesome", + "**/mochawesome/**", + "**/mocha-multi-reporters", + "**/mocha-multi-reporters/**" + ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 898fcd4eb41fa9..22735bda893c74 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2556,10 +2556,8 @@ "kbnVislibVisTypes.editors.heatmap.showTooltipsLabel": "ツールヒントを表示", "kbnVislibVisTypes.editors.pie.donutLabel": "ドーナッツ", "kbnVislibVisTypes.editors.pointSeries.currentTimeMarkerLabel": "現在時刻マーカー", - "kbnVislibVisTypes.editors.pointSeries.legendPositionLabel": "凡例の配置", "kbnVislibVisTypes.editors.pointSeries.orderBucketsBySumLabel": "バケットを合計で並べ替え", "kbnVislibVisTypes.editors.pointSeries.settingsTitle": "設定", - "kbnVislibVisTypes.editors.pointSeries.showTooltipLabel": "ツールヒントを表示", "kbnVislibVisTypes.functions.pie.help": "パイビジュアライゼーション", "kbnVislibVisTypes.functions.vislib.help": "Vislib ビジュアライゼーション", "kbnVislibVisTypes.gauge.alignmentAutomaticTitle": "自動", @@ -3907,7 +3905,6 @@ "xpack.apm.transactions.chart.averageLabel": "平均", "xpack.apm.transactionsTable.95thPercentileColumnLabel": "95 パーセンタイル", "xpack.apm.transactionsTable.avgDurationColumnLabel": "平均期間", - "xpack.apm.transactionsTable.filterByTypeLabel": "タイプでフィルタリング", "xpack.apm.transactionsTable.impactColumnLabel": "インパクト", "xpack.apm.transactionsTable.nameColumnLabel": "名前", "xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel": "1 分あたりのトランザクション", @@ -4892,7 +4889,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "新規ポリシーとして保存します", "xpack.indexLifecycleMgmt.editPolicy.saveButton": "ポリシーを保存", "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "ライフサイクルポリシー {lifecycleName} の保存中にエラーが発生しました", - "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButton": "JSON を表示", "xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage": "ライフサイクルポリシー「{lifecycleName}」を {verb}", "xpack.indexLifecycleMgmt.editPolicy.updatedMessage": "更新しました", "xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage": "ポリシー名の頭にアンダーラインを使用することはできず、括弧やスペースを含めることもできません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c47454d87c3839..84a797e2b185ba 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2556,10 +2556,8 @@ "kbnVislibVisTypes.editors.heatmap.showTooltipsLabel": "显示工具提示", "kbnVislibVisTypes.editors.pie.donutLabel": "圆环图", "kbnVislibVisTypes.editors.pointSeries.currentTimeMarkerLabel": "当前时间标记", - "kbnVislibVisTypes.editors.pointSeries.legendPositionLabel": "图例位置", "kbnVislibVisTypes.editors.pointSeries.orderBucketsBySumLabel": "按总计值排序桶", "kbnVislibVisTypes.editors.pointSeries.settingsTitle": "设置", - "kbnVislibVisTypes.editors.pointSeries.showTooltipLabel": "显示工具提示", "kbnVislibVisTypes.functions.pie.help": "饼图可视化", "kbnVislibVisTypes.functions.vislib.help": "Vislib 可视化", "kbnVislibVisTypes.gauge.alignmentAutomaticTitle": "自动", @@ -3907,7 +3905,6 @@ "xpack.apm.transactions.chart.averageLabel": "平均", "xpack.apm.transactionsTable.95thPercentileColumnLabel": "第 95 个百分位", "xpack.apm.transactionsTable.avgDurationColumnLabel": "平均持续时间", - "xpack.apm.transactionsTable.filterByTypeLabel": "按类型筛选", "xpack.apm.transactionsTable.impactColumnLabel": "影响", "xpack.apm.transactionsTable.nameColumnLabel": "名称", "xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel": "每分钟事务数", @@ -5035,7 +5032,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "另存为新策略", "xpack.indexLifecycleMgmt.editPolicy.saveButton": "保存策略", "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "保存生命周期策略 {lifecycleName} 时出错", - "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButton": "显示 JSON", "xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage": "{verb}生命周期策略“{lifecycleName}”", "xpack.indexLifecycleMgmt.editPolicy.updatedMessage": "已更新", "xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage": "策略名称不能以下划线开头,且不能包含问号或空格。", @@ -10659,4 +10655,4 @@ "xpack.watcher.watchActions.logging.logTextIsRequiredValidationMessage": "“日志文本”必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/email.ts index bbe512700be08a..b253fbdc7f8452 100644 --- a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/email.ts @@ -71,7 +71,7 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should return the message data when firing the __json service', async () => { await supertest - .post(`/api/action/${createdActionId}/_fire`) + .post(`/api/action/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -114,7 +114,7 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should render html from markdown', async () => { await supertest - .post(`/api/action/${createdActionId}/_fire`) + .post(`/api/action/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/es_index.ts index 9f77be490bb95e..6e94b69b2cd808 100644 --- a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/es_index.ts @@ -115,9 +115,9 @@ export default function indexTest({ getService }: FtrProviderContext) { }); }); - it('should fire successly when expected for a single body', async () => { + it('should execute successly when expected for a single body', async () => { const { body: result } = await supertest - .post(`/api/action/${createdActionID}/_fire`) + .post(`/api/action/${createdActionID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -134,9 +134,9 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(items[0]._source).to.eql({ testing: [1, 2, 3] }); }); - it('should fire successly when expected for with multiple bodies', async () => { + it('should execute successly when expected for with multiple bodies', async () => { const { body: result } = await supertest - .post(`/api/action/${createdActionID}/_fire`) + .post(`/api/action/${createdActionID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -167,9 +167,9 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(passed2).to.be(true); }); - it('should fire successly with refresh false', async () => { + it('should execute successly with refresh false', async () => { const { body: result } = await supertest - .post(`/api/action/${createdActionID}/_fire`) + .post(`/api/action/${createdActionID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -185,7 +185,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(items.length).to.be.lessThan(2); const { body: result2 } = await supertest - .post(`/api/action/${createdActionID}/_fire`) + .post(`/api/action/${createdActionID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -201,12 +201,12 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(items.length).to.eql(2); }); - it('should fire unsuccessfully when expected', async () => { + it('should execute unsuccessfully when expected', async () => { let response; let result; response = await supertest - .post(`/api/action/${createdActionID}/_fire`) + .post(`/api/action/${createdActionID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -222,7 +222,7 @@ export default function indexTest({ getService }: FtrProviderContext) { ); response = await supertest - .post(`/api/action/${createdActionID}/_fire`) + .post(`/api/action/${createdActionID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/server_log.ts index e56555e1094de0..926a8e887434e3 100644 --- a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/server_log.ts @@ -51,7 +51,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { it('should handle firing the action', async () => { const { body: result } = await supertest - .post(`/api/action/${serverLogActionId}/_fire`) + .post(`/api/action/${serverLogActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/slack.ts index 8a6f0a7e27109f..c09667ff5670d1 100644 --- a/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/apis/actions/builtin_action_types/slack.ts @@ -99,7 +99,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle firing with a simulated success', async () => { const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_fire`) + .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -112,7 +112,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle a 40x slack error', async () => { const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_fire`) + .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -127,7 +127,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle a 429 slack error', async () => { const dateStart = new Date().getTime(); const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_fire`) + .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -146,7 +146,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle a 500 slack error', async () => { const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_fire`) + .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/apis/actions/fire.ts b/x-pack/test/alerting_api_integration/apis/actions/execute.ts similarity index 86% rename from x-pack/test/alerting_api_integration/apis/actions/fire.ts rename to x-pack/test/alerting_api_integration/apis/actions/execute.ts index 5525f0db89a358..919c0183786909 100644 --- a/x-pack/test/alerting_api_integration/apis/actions/fire.ts +++ b/x-pack/test/alerting_api_integration/apis/actions/execute.ts @@ -16,7 +16,7 @@ export default function({ getService }: FtrProviderContext) { const esTestIndexName = '.kibaka-alerting-test-data'; - describe('fire', () => { + describe('execute', () => { beforeEach(() => esArchiver.load('actions/basic')); afterEach(() => esArchiver.unload('actions/basic')); @@ -52,14 +52,14 @@ export default function({ getService }: FtrProviderContext) { }); after(() => es.indices.delete({ index: esTestIndexName })); - it('decrypts attributes when calling fire API', async () => { + it('decrypts attributes when calling execute API', async () => { await supertest - .post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`) + .post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { index: esTestIndexName, - reference: 'actions-fire-1', + reference: 'actions-execute-1', message: 'Testing 123', }, }) @@ -81,7 +81,7 @@ export default function({ getService }: FtrProviderContext) { }, { term: { - reference: 'actions-fire-1', + reference: 'actions-execute-1', }, }, ], @@ -95,7 +95,7 @@ export default function({ getService }: FtrProviderContext) { expect(indexedRecord._source).to.eql({ params: { index: esTestIndexName, - reference: 'actions-fire-1', + reference: 'actions-execute-1', message: 'Testing 123', }, config: { @@ -104,33 +104,33 @@ export default function({ getService }: FtrProviderContext) { secrets: { encrypted: 'This value should be encrypted', }, - reference: 'actions-fire-1', + reference: 'actions-execute-1', source: 'action:test.index-record', }); }); - it(`can't fire from another space`, async () => { + it(`can't execute from another space`, async () => { await supertest - .post(`/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}/_fire`) + .post(`/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { index: esTestIndexName, - reference: 'actions-fire-2', + reference: 'actions-execute-2', message: 'Testing 123', }, }) .expect(404); }); - it('fire works in a space', async () => { + it('execute works in a space', async () => { await supertest - .post(`/s/space_1/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}/_fire`) + .post(`/s/space_1/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { index: esTestIndexName, - reference: 'actions-fire-3', + reference: 'actions-execute-3', message: 'Testing 123', }, }) @@ -152,7 +152,7 @@ export default function({ getService }: FtrProviderContext) { }, { term: { - reference: 'actions-fire-3', + reference: 'actions-execute-3', }, }, ], @@ -166,7 +166,7 @@ export default function({ getService }: FtrProviderContext) { expect(indexedRecord._source).to.eql({ params: { index: esTestIndexName, - reference: 'actions-fire-3', + reference: 'actions-execute-3', message: 'Testing 123', }, config: { @@ -175,12 +175,12 @@ export default function({ getService }: FtrProviderContext) { secrets: { encrypted: 'This value should be encrypted', }, - reference: 'actions-fire-3', + reference: 'actions-execute-3', source: 'action:test.index-record', }); }); - it('fire still works with encrypted attributes after updating an action', async () => { + it('execute still works with encrypted attributes after updating an action', async () => { const { body: updatedAction } = await supertest .put(`/api/action/${ES_ARCHIVER_ACTION_ID}`) .set('kbn-xsrf', 'foo') @@ -203,12 +203,12 @@ export default function({ getService }: FtrProviderContext) { }, }); await supertest - .post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`) + .post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { index: esTestIndexName, - reference: 'actions-fire-4', + reference: 'actions-execute-4', message: 'Testing 123', }, }) @@ -230,7 +230,7 @@ export default function({ getService }: FtrProviderContext) { }, { term: { - reference: 'actions-fire-4', + reference: 'actions-execute-4', }, }, ], @@ -244,7 +244,7 @@ export default function({ getService }: FtrProviderContext) { expect(indexedRecord._source).to.eql({ params: { index: esTestIndexName, - reference: 'actions-fire-4', + reference: 'actions-execute-4', message: 'Testing 123', }, config: { @@ -253,14 +253,14 @@ export default function({ getService }: FtrProviderContext) { secrets: { encrypted: 'This value should be encrypted', }, - reference: 'actions-fire-4', + reference: 'actions-execute-4', source: 'action:test.index-record', }); }); it(`should return 404 when action doesn't exist`, async () => { const { body: response } = await supertest - .post('/api/action/1/_fire') + .post('/api/action/1/_execute') .set('kbn-xsrf', 'foo') .send({ params: { foo: true }, @@ -275,7 +275,7 @@ export default function({ getService }: FtrProviderContext) { it('should return 400 when payload is empty and invalid', async () => { const { body: response } = await supertest - .post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`) + .post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_execute`) .set('kbn-xsrf', 'foo') .send({}) .expect(400); diff --git a/x-pack/test/alerting_api_integration/apis/actions/index.ts b/x-pack/test/alerting_api_integration/apis/actions/index.ts index 9a1a21cf29676c..66e83563f802a0 100644 --- a/x-pack/test/alerting_api_integration/apis/actions/index.ts +++ b/x-pack/test/alerting_api_integration/apis/actions/index.ts @@ -14,7 +14,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./fire')); + loadTestFile(require.resolve('./execute')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/slack')); loadTestFile(require.resolve('./builtin_action_types/email')); diff --git a/x-pack/test/alerting_api_integration/apis/actions/manual/pr_40694.js b/x-pack/test/alerting_api_integration/apis/actions/manual/pr_40694.js index 79bd8f7c5b2018..bc233bfb629d5a 100755 --- a/x-pack/test/alerting_api_integration/apis/actions/manual/pr_40694.js +++ b/x-pack/test/alerting_api_integration/apis/actions/manual/pr_40694.js @@ -53,7 +53,7 @@ async function main() { response = await httpGet(`api/action/${actionId}`); console.log(`action after update: ${JSON.stringify(response, null, 4)}`); - response = await httpPost(`api/action/${actionId}/_fire`, { + response = await httpPost(`api/action/${actionId}/_execute`, { params: { to: ['patrick.mueller@elastic.co'], subject: 'the email subject', @@ -61,7 +61,7 @@ async function main() { } }); - console.log(`fire result: ${JSON.stringify(response, null, 4)}`); + console.log(`execute result: ${JSON.stringify(response, null, 4)}`); } async function httpGet(uri) { diff --git a/x-pack/test/alerting_api_integration/apis/actions/update.ts b/x-pack/test/alerting_api_integration/apis/actions/update.ts index d68d1526d9eeb6..8979bbbbd45fda 100644 --- a/x-pack/test/alerting_api_integration/apis/actions/update.ts +++ b/x-pack/test/alerting_api_integration/apis/actions/update.ts @@ -244,9 +244,9 @@ export default function updateActionTests({ getService }: FtrProviderContext) { }) .expect(200); - // fire the action + // execute the action await supertest - .post(`/api/action/${emailActionId}/_fire`) + .post(`/api/action/${emailActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/api_integration/apis/infra/metadata.ts b/x-pack/test/api_integration/apis/infra/metadata.ts index 546403b8b19349..ac3fa06d958dde 100644 --- a/x-pack/test/api_integration/apis/infra/metadata.ts +++ b/x-pack/test/api_integration/apis/infra/metadata.ts @@ -64,180 +64,216 @@ export default function({ getService }: FtrProviderContext) { }); describe('8.0.0', () => { - const archiveName = 'infra/8.0.0/logs_and_metrics_with_aws'; - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + describe('cloud and host information', () => { + const archiveName = 'infra/8.0.0/logs_and_metrics_with_aws'; + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); - it('host', async () => { - const metadata = await fetchMetadata({ - sourceId: 'default', - nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc', - nodeType: InfraNodeType.host, - }); - if (metadata) { - expect(metadata.features.length).to.be(58); - expect(metadata.name).to.equal('gke-observability-8--observability-8--bc1afd95-f0zc'); - expect(metadata.info).to.eql({ - cloud: { - availability_zone: 'europe-west1-c', - instance: { - name: 'gke-observability-8--observability-8--bc1afd95-f0zc', - id: '6200309808276807579', + it('host', async () => { + const metadata = await fetchMetadata({ + sourceId: 'default', + nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc', + nodeType: InfraNodeType.host, + }); + if (metadata) { + expect(metadata.features.length).to.be(58); + expect(metadata.name).to.equal('gke-observability-8--observability-8--bc1afd95-f0zc'); + expect(metadata.info).to.eql({ + cloud: { + availability_zone: 'europe-west1-c', + instance: { + name: 'gke-observability-8--observability-8--bc1afd95-f0zc', + id: '6200309808276807579', + }, + provider: 'gcp', + machine: { type: 'n1-standard-4' }, + project: { id: 'elastic-observability' }, }, - provider: 'gcp', - machine: { type: 'n1-standard-4' }, - project: { id: 'elastic-observability' }, - }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', - os: { - kernel: '4.14.127+', - codename: 'Core', - name: 'CentOS Linux', - family: 'redhat', - version: '7 (Core)', - platform: 'centos', + host: { + hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', + os: { + kernel: '4.14.127+', + codename: 'Core', + name: 'CentOS Linux', + family: 'redhat', + version: '7 (Core)', + platform: 'centos', + }, + containerized: false, + name: 'gke-observability-8--observability-8--bc1afd95-f0zc', + architecture: 'x86_64', }, - containerized: false, - name: 'gke-observability-8--observability-8--bc1afd95-f0zc', - architecture: 'x86_64', - }, - }); - } else { - throw new Error('Metadata should never be empty'); - } - }); - - it('host with aws', async () => { - const metadata = await fetchMetadata({ - sourceId: 'default', - nodeId: 'ip-172-31-47-9.us-east-2.compute.internal', - nodeType: InfraNodeType.host, + }); + } else { + throw new Error('Metadata should never be empty'); + } }); - if (metadata) { - expect(metadata.features.length).to.be(19); - expect(metadata.features.some(f => f.name === 'aws.ec2')).to.be(true); - expect(metadata.name).to.equal('ip-172-31-47-9.us-east-2.compute.internal'); - expect(metadata.info).to.eql({ - cloud: { - availability_zone: 'us-east-2c', - image: { id: 'ami-0d8f6eb4f641ef691' }, - instance: { id: 'i-011454f72559c510b' }, - provider: 'aws', - machine: { type: 't2.micro' }, - region: 'us-east-2', - account: { id: '015351775590' }, - }, - host: { - hostname: 'ip-172-31-47-9.us-east-2.compute.internal', - os: { - kernel: '4.14.123-111.109.amzn2.x86_64', - codename: 'Karoo', - name: 'Amazon Linux', - family: 'redhat', - version: '2', - platform: 'amzn', - }, - containerized: false, - name: 'ip-172-31-47-9.us-east-2.compute.internal', - id: 'ded64cbff86f478990a3dfbb63a8d238', - architecture: 'x86_64', - }, - }); - } else { - throw new Error('Metadata should never be empty'); - } - }); - it('pod', async () => { - const metadata = await fetchMetadata({ - sourceId: 'default', - nodeId: '14887487-99f8-11e9-9a96-42010a84004d', - nodeType: InfraNodeType.pod, - }); - if (metadata) { - expect(metadata.features.length).to.be(29); - // With this data set the `kubernetes.pod.name` fields have been removed. - expect(metadata.name).to.equal('fluentd-gcp-v3.2.0-np7vw'); - expect(metadata.info).to.eql({ - cloud: { - instance: { - id: '6613144177892233360', - name: 'gke-observability-8--observability-8--bc1afd95-ngmh', + it('host with aws', async () => { + const metadata = await fetchMetadata({ + sourceId: 'default', + nodeId: 'ip-172-31-47-9.us-east-2.compute.internal', + nodeType: InfraNodeType.host, + }); + if (metadata) { + expect(metadata.features.length).to.be(19); + expect(metadata.features.some(f => f.name === 'aws.ec2')).to.be(true); + expect(metadata.name).to.equal('ip-172-31-47-9.us-east-2.compute.internal'); + expect(metadata.info).to.eql({ + cloud: { + availability_zone: 'us-east-2c', + image: { id: 'ami-0d8f6eb4f641ef691' }, + instance: { id: 'i-011454f72559c510b' }, + provider: 'aws', + machine: { type: 't2.micro' }, + region: 'us-east-2', + account: { id: '015351775590' }, }, - provider: 'gcp', - availability_zone: 'europe-west1-c', - machine: { - type: 'n1-standard-4', + host: { + hostname: 'ip-172-31-47-9.us-east-2.compute.internal', + os: { + kernel: '4.14.123-111.109.amzn2.x86_64', + codename: 'Karoo', + name: 'Amazon Linux', + family: 'redhat', + version: '2', + platform: 'amzn', + }, + containerized: false, + name: 'ip-172-31-47-9.us-east-2.compute.internal', + id: 'ded64cbff86f478990a3dfbb63a8d238', + architecture: 'x86_64', }, - project: { - id: 'elastic-observability', + }); + } else { + throw new Error('Metadata should never be empty'); + } + }); + + it('pod', async () => { + const metadata = await fetchMetadata({ + sourceId: 'default', + nodeId: '14887487-99f8-11e9-9a96-42010a84004d', + nodeType: InfraNodeType.pod, + }); + if (metadata) { + expect(metadata.features.length).to.be(29); + // With this data set the `kubernetes.pod.name` fields have been removed. + expect(metadata.name).to.equal('fluentd-gcp-v3.2.0-np7vw'); + expect(metadata.info).to.eql({ + cloud: { + instance: { + id: '6613144177892233360', + name: 'gke-observability-8--observability-8--bc1afd95-ngmh', + }, + provider: 'gcp', + availability_zone: 'europe-west1-c', + machine: { + type: 'n1-standard-4', + }, + project: { + id: 'elastic-observability', + }, }, - }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', - name: 'gke-observability-8--observability-8--bc1afd95-ngmh', - os: { - codename: 'Core', - family: 'redhat', - kernel: '4.14.127+', - name: 'CentOS Linux', - platform: 'centos', - version: '7 (Core)', + host: { + hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', + name: 'gke-observability-8--observability-8--bc1afd95-ngmh', + os: { + codename: 'Core', + family: 'redhat', + kernel: '4.14.127+', + name: 'CentOS Linux', + platform: 'centos', + version: '7 (Core)', + }, + architecture: 'x86_64', + containerized: false, }, - architecture: 'x86_64', - containerized: false, - }, + }); + } else { + throw new Error('Metadata should never be empty'); + } + }); + + it('container', async () => { + const metadata = await fetchMetadata({ + sourceId: 'default', + nodeId: 'c74b04834c6d7cc1800c3afbe31d0c8c0c267f06e9eb45c2b0c2df3e6cee40c5', + nodeType: InfraNodeType.container, }); - } else { - throw new Error('Metadata should never be empty'); - } + if (metadata) { + expect(metadata.features.length).to.be(26); + expect(metadata.name).to.equal( + 'k8s_prometheus-to-sd-exporter_fluentd-gcp-v3.2.0-w68r5_kube-system_26950cde-9aed-11e9-9a96-42010a84004d_0' + ); + expect(metadata.info).to.eql({ + cloud: { + instance: { + id: '4039094952262994102', + name: 'gke-observability-8--observability-8--bc1afd95-nhhw', + }, + provider: 'gcp', + availability_zone: 'europe-west1-c', + machine: { + type: 'n1-standard-4', + }, + project: { + id: 'elastic-observability', + }, + }, + host: { + hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', + name: 'gke-observability-8--observability-8--bc1afd95-nhhw', + os: { + codename: 'Core', + family: 'redhat', + kernel: '4.14.127+', + name: 'CentOS Linux', + platform: 'centos', + version: '7 (Core)', + }, + architecture: 'x86_64', + containerized: false, + }, + }); + } else { + throw new Error('Metadata should never be empty'); + } + }); }); + describe('APM metrics', () => { + const archiveName = 'infra/8.0.0/metrics_and_apm'; + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); - it('container', async () => { - const metadata = await fetchMetadata({ - sourceId: 'default', - nodeId: 'c74b04834c6d7cc1800c3afbe31d0c8c0c267f06e9eb45c2b0c2df3e6cee40c5', - nodeType: InfraNodeType.container, + it('host without APM data', async () => { + const metadata = await fetchMetadata({ + sourceId: 'default', + nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc', + nodeType: 'host', + }); + if (metadata) { + expect( + metadata.features.some(f => f.name === 'apm.transaction' && f.source === 'apm') + ).to.be(false); + } else { + throw new Error('Metadata should never be empty'); + } }); - if (metadata) { - expect(metadata.features.length).to.be(26); - expect(metadata.name).to.equal( - 'k8s_prometheus-to-sd-exporter_fluentd-gcp-v3.2.0-w68r5_kube-system_26950cde-9aed-11e9-9a96-42010a84004d_0' - ); - expect(metadata.info).to.eql({ - cloud: { - instance: { - id: '4039094952262994102', - name: 'gke-observability-8--observability-8--bc1afd95-nhhw', - }, - provider: 'gcp', - availability_zone: 'europe-west1-c', - machine: { - type: 'n1-standard-4', - }, - project: { - id: 'elastic-observability', - }, - }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', - name: 'gke-observability-8--observability-8--bc1afd95-nhhw', - os: { - codename: 'Core', - family: 'redhat', - kernel: '4.14.127+', - name: 'CentOS Linux', - platform: 'centos', - version: '7 (Core)', - }, - architecture: 'x86_64', - containerized: false, - }, + it('pod with APM data', async () => { + const metadata = await fetchMetadata({ + sourceId: 'default', + nodeId: 'c1031331-9ae0-11e9-9a96-42010a84004d', + nodeType: 'pod', }); - } else { - throw new Error('Metadata should never be empty'); - } + if (metadata) { + expect( + metadata.features.some(f => f.name === 'apm.transaction' && f.source === 'apm') + ).to.be(true); + } else { + throw new Error('Metadata should never be empty'); + } + }); }); }); }); diff --git a/x-pack/test/functional/apps/machine_learning/index.ts b/x-pack/test/functional/apps/machine_learning/index.ts index 31f7d67717ae3a..d3d96d63b55d90 100644 --- a/x-pack/test/functional/apps/machine_learning/index.ts +++ b/x-pack/test/functional/apps/machine_learning/index.ts @@ -10,7 +10,9 @@ export default function({ loadTestFile }: FtrProviderContext) { this.tags('ciGroup3'); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./create_single_metric_job')); + + // FLAKY: https://github.com/elastic/kibana/issues/43017 + // loadTestFile(require.resolve('./pages')); + // loadTestFile(require.resolve('./create_single_metric_job')); }); } diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm/data.json.gz b/x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm/data.json.gz new file mode 100644 index 00000000000000..0a6175945b0d9d Binary files /dev/null and b/x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm/mappings.json b/x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm/mappings.json new file mode 100644 index 00000000000000..36188ebdaba9e1 --- /dev/null +++ b/x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm/mappings.json @@ -0,0 +1,17627 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "apm-2019.07.29", + "mappings": { + "_meta": { + "beat": "apm", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "transaction.marks": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "transaction.marks.*" + } + }, + { + "transaction.marks.*.*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "transaction.marks.*.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "dynamic": "false", + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "dynamic": "false", + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "dynamic": "false", + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "culprit": { + "ignore_above": 1024, + "type": "keyword" + }, + "exception": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handled": { + "type": "boolean" + }, + "message": { + "norms": false, + "type": "text" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "grouping_key": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "param_message": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "experimental": { + "dynamic": "true", + "type": "object" + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "target_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "golang": { + "properties": { + "goroutines": { + "type": "long" + }, + "heap": { + "properties": { + "allocations": { + "properties": { + "active": { + "type": "long" + }, + "allocated": { + "type": "long" + }, + "frees": { + "type": "long" + }, + "idle": { + "type": "long" + }, + "mallocs": { + "type": "long" + }, + "objects": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "gc": { + "properties": { + "cpu_fraction": { + "type": "float" + }, + "next_gc_limit": { + "type": "long" + }, + "total_count": { + "type": "long" + }, + "total_pause": { + "properties": { + "ns": { + "type": "long" + } + } + } + } + }, + "system": { + "properties": { + "obtained": { + "type": "long" + }, + "released": { + "type": "long" + }, + "stack": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + } + } + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "dynamic": "false", + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "dynamic": "false", + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "finished": { + "type": "boolean" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jvm": { + "properties": { + "gc": { + "properties": { + "alloc": { + "type": "long" + }, + "count": { + "type": "long" + }, + "time": { + "type": "long" + } + } + }, + "memory": { + "properties": { + "heap": { + "properties": { + "committed": { + "type": "long" + }, + "max": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "non_heap": { + "properties": { + "committed": { + "type": "long" + }, + "max": { + "type": "long" + }, + "used": { + "type": "long" + } + } + } + } + }, + "thread": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "kubernetes": { + "dynamic": "false", + "properties": { + "annotations": { + "type": "object" + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "dynamic": "true", + "properties": { + "company": { + "type": "keyword" + }, + "customer_email": { + "type": "keyword" + }, + "customer_name": { + "type": "keyword" + }, + "customer_tier": { + "type": "keyword" + }, + "env": { + "type": "keyword" + }, + "foo": { + "type": "keyword" + }, + "hostname": { + "type": "keyword" + }, + "lorem": { + "type": "keyword" + }, + "multi-line": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "request_id": { + "type": "keyword" + }, + "served_from_cache": { + "type": "keyword" + }, + "this-is-a-very-long-tag-name-without-any-spaces": { + "type": "keyword" + } + } + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "observer": { + "dynamic": "false", + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "listening": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_major": { + "type": "byte" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "parent": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "dynamic": "false", + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "processor": { + "properties": { + "event": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "ip": { + "type": "ip" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "dynamic": "false", + "properties": { + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "framework": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "language": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "sourcemap": { + "dynamic": "false", + "properties": { + "bundle_filepath": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "span": { + "dynamic": "false", + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "start": { + "properties": { + "us": { + "type": "long" + } + } + }, + "subtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "sync": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "system": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "actual": { + "properties": { + "free": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "process": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "size": { + "type": "long" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timestamp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "trace": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "dynamic": "false", + "properties": { + "breakdown": { + "properties": { + "count": { + "type": "long" + } + } + }, + "duration": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + }, + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "marks": { + "dynamic": "true", + "properties": { + "*": { + "properties": { + "*": { + "dynamic": "true", + "type": "object" + } + } + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "sampled": { + "type": "boolean" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "span_count": { + "properties": { + "dropped": { + "type": "long" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "dynamic": "false", + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "dynamic": "false", + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "view spans": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "query": { + "default_field": [ + "beat.*", + "type", + "tags", + "meta.*", + "message" + ] + } + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "metricbeat-2019.07.29", + "mappings": { + "_meta": { + "beat": "metricbeat", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "aws.tags.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "aws.tags.*" + } + }, + { + "aws.cloudwatch.metrics.*": { + "mapping": { + "type": "double" + }, + "path_match": "aws.cloudwatch.metrics.*" + } + }, + { + "aws.cloudwatch.dimensions.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "aws.cloudwatch.dimensions.*" + } + }, + { + "coredns.stats.dns.request.duration.ns.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "coredns.stats.dns.request.duration.ns.bucket.*" + } + }, + { + "coredns.stats.dns.request.size.bytes.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "coredns.stats.dns.request.size.bytes.bucket.*" + } + }, + { + "coredns.stats.dns.response.size.bytes.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "coredns.stats.dns.response.size.bytes.bucket.*" + } + }, + { + "docker.cpu.core.*.pct": { + "mapping": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "path_match": "docker.cpu.core.*.pct" + } + }, + { + "docker.cpu.core.*.ticks": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "docker.cpu.core.*.ticks" + } + }, + { + "docker.event.actor.attributes": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.event.actor.attributes.*" + } + }, + { + "docker.image.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.image.labels.*" + } + }, + { + "etcd.disk.wal_fsync_duration.ns.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "etcd.disk.wal_fsync_duration.ns.bucket.*" + } + }, + { + "etcd.disk.backend_commit_duration.ns.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "etcd.disk.backend_commit_duration.ns.bucket.*" + } + }, + { + "kubernetes.apiserver.http.request.duration.us.percentile.*": { + "mapping": { + "type": "double" + }, + "match_mapping_type": "double", + "path_match": "kubernetes.apiserver.http.request.duration.us.percentile.*" + } + }, + { + "kubernetes.apiserver.http.request.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.apiserver.http.request.size.bytes.percentile.*" + } + }, + { + "kubernetes.apiserver.http.response.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.apiserver.http.response.size.bytes.percentile.*" + } + }, + { + "kubernetes.apiserver.request.latency.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.apiserver.request.latency.bucket.*" + } + }, + { + "kubernetes.apiserver.request.duration.us.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.apiserver.request.duration.us.bucket.*" + } + }, + { + "kubernetes.controllermanager.http.request.duration.us.percentile.*": { + "mapping": { + "type": "double" + }, + "match_mapping_type": "double", + "path_match": "kubernetes.controllermanager.http.request.duration.us.percentile.*" + } + }, + { + "kubernetes.controllermanager.http.request.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.controllermanager.http.request.size.bytes.percentile.*" + } + }, + { + "kubernetes.controllermanager.http.response.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.controllermanager.http.response.size.bytes.percentile.*" + } + }, + { + "kubernetes.proxy.http.request.duration.us.percentile.*": { + "mapping": { + "type": "double" + }, + "match_mapping_type": "double", + "path_match": "kubernetes.proxy.http.request.duration.us.percentile.*" + } + }, + { + "kubernetes.proxy.http.request.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.proxy.http.request.size.bytes.percentile.*" + } + }, + { + "kubernetes.proxy.http.response.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.proxy.http.response.size.bytes.percentile.*" + } + }, + { + "kubernetes.proxy.sync.rules.duration.us.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.proxy.sync.rules.duration.us.bucket.*" + } + }, + { + "kubernetes.proxy.sync.networkprogramming.duration.us.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.proxy.sync.networkprogramming.duration.us.bucket.*" + } + }, + { + "kubernetes.scheduler.http.request.duration.us.percentile.*": { + "mapping": { + "type": "double" + }, + "match_mapping_type": "double", + "path_match": "kubernetes.scheduler.http.request.duration.us.percentile.*" + } + }, + { + "kubernetes.scheduler.http.request.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.scheduler.http.request.size.bytes.percentile.*" + } + }, + { + "kubernetes.scheduler.http.response.size.bytes.percentile.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.scheduler.http.response.size.bytes.percentile.*" + } + }, + { + "kubernetes.scheduler.scheduling.e2e.duration.us.bucket.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "kubernetes.scheduler.scheduling.e2e.duration.us.bucket.*" + } + }, + { + "kubernetes.scheduler.scheduling.duration.seconds.percentile.*": { + "mapping": { + "type": "double" + }, + "match_mapping_type": "double", + "path_match": "kubernetes.scheduler.scheduling.duration.seconds.percentile.*" + } + }, + { + "munin.metrics.*": { + "mapping": { + "type": "double" + }, + "path_match": "munin.metrics.*" + } + }, + { + "prometheus.labels.*": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "prometheus.labels.*" + } + }, + { + "prometheus.metrics.*": { + "mapping": { + "type": "double" + }, + "path_match": "prometheus.metrics.*" + } + }, + { + "system.process.env": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "system.process.env.*" + } + }, + { + "system.process.cgroup.cpuacct.percpu": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "system.process.cgroup.cpuacct.percpu.*" + } + }, + { + "system.raid.disks.states.*": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "system.raid.disks.states.*" + } + }, + { + "traefik.health.response.status_codes.*": { + "mapping": { + "type": "long" + }, + "match_mapping_type": "long", + "path_match": "traefik.health.response.status_codes.*" + } + }, + { + "vsphere.virtualmachine.custom_fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "vsphere.virtualmachine.custom_fields.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "aerospike": { + "properties": { + "namespace": { + "properties": { + "client": { + "properties": { + "delete": { + "properties": { + "error": { + "type": "long" + }, + "not_found": { + "type": "long" + }, + "success": { + "type": "long" + }, + "timeout": { + "type": "long" + } + } + }, + "read": { + "properties": { + "error": { + "type": "long" + }, + "not_found": { + "type": "long" + }, + "success": { + "type": "long" + }, + "timeout": { + "type": "long" + } + } + }, + "write": { + "properties": { + "error": { + "type": "long" + }, + "success": { + "type": "long" + }, + "timeout": { + "type": "long" + } + } + } + } + }, + "device": { + "properties": { + "available": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "free": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "hwm_breached": { + "type": "boolean" + }, + "memory": { + "properties": { + "free": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "used": { + "properties": { + "data": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "index": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "sindex": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "objects": { + "properties": { + "master": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "stop_writes": { + "type": "boolean" + } + } + } + } + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "apache": { + "properties": { + "status": { + "properties": { + "bytes_per_request": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "bytes_per_sec": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "connections": { + "properties": { + "async": { + "properties": { + "closing": { + "type": "long" + }, + "keep_alive": { + "type": "long" + }, + "writing": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "cpu": { + "properties": { + "children_system": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "children_user": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "load": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "system": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "user": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "load": { + "properties": { + "1": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "15": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "5": { + "scaling_factor": 100, + "type": "scaled_float" + } + } + }, + "requests_per_sec": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "scoreboard": { + "properties": { + "closing_connection": { + "type": "long" + }, + "dns_lookup": { + "type": "long" + }, + "gracefully_finishing": { + "type": "long" + }, + "idle_cleanup": { + "type": "long" + }, + "keepalive": { + "type": "long" + }, + "logging": { + "type": "long" + }, + "open_slot": { + "type": "long" + }, + "reading_request": { + "type": "long" + }, + "sending_reply": { + "type": "long" + }, + "starting_up": { + "type": "long" + }, + "total": { + "type": "long" + }, + "waiting_for_connection": { + "type": "long" + } + } + }, + "total_accesses": { + "type": "long" + }, + "total_kbytes": { + "type": "long" + }, + "uptime": { + "properties": { + "server_uptime": { + "type": "long" + }, + "uptime": { + "type": "long" + } + } + }, + "workers": { + "properties": { + "busy": { + "type": "long" + }, + "idle": { + "type": "long" + } + } + } + } + } + } + }, + "aws": { + "properties": { + "cloudwatch": { + "properties": { + "dimensions": { + "properties": { + "*": { + "type": "object" + } + } + }, + "metrics": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ec2": { + "properties": { + "cpu": { + "properties": { + "credit_balance": { + "type": "long" + }, + "credit_usage": { + "type": "long" + }, + "surplus_credit_balance": { + "type": "long" + }, + "surplus_credits_charged": { + "type": "long" + }, + "total": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "diskio": { + "properties": { + "read": { + "properties": { + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + }, + "ops": { + "type": "long" + } + } + }, + "write": { + "properties": { + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + }, + "ops": { + "type": "long" + } + } + } + } + }, + "instance": { + "properties": { + "core": { + "properties": { + "count": { + "type": "long" + } + } + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "monitoring": { + "properties": { + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "private": { + "properties": { + "dns_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + } + } + }, + "public": { + "properties": { + "dns_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + } + } + }, + "state": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "threads_per_core": { + "type": "long" + } + } + }, + "network": { + "properties": { + "in": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + }, + "out": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + } + } + }, + "status": { + "properties": { + "check_failed": { + "type": "long" + }, + "check_failed_instance": { + "type": "long" + }, + "check_failed_system": { + "type": "long" + } + } + } + } + }, + "rds": { + "properties": { + "cpu": { + "properties": { + "credit_balance": { + "type": "long" + }, + "credit_usage": { + "type": "long" + }, + "total": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "database_connections": { + "type": "long" + }, + "db_instance": { + "properties": { + "arn": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deadlocks": { + "type": "long" + }, + "disk_queue_depth": { + "type": "long" + }, + "disk_usage": { + "properties": { + "bin_log": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "replication_slot": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "transaction_logs": { + "properties": { + "mb": { + "type": "long" + } + } + } + } + }, + "failed_sql_server_agent_jobs": { + "type": "long" + }, + "free_local_storage": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "free_storage": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "freeable_memory": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "latency": { + "properties": { + "commit": { + "type": "long" + }, + "ddl": { + "type": "long" + }, + "dml": { + "type": "long" + }, + "insert": { + "type": "long" + }, + "read": { + "type": "long" + }, + "select": { + "type": "long" + }, + "update": { + "type": "long" + }, + "write": { + "type": "long" + } + } + }, + "login_failures": { + "type": "long" + }, + "maximum_used_transaction_ids": { + "type": "long" + }, + "oldest_replication_slot_lag": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "queries": { + "type": "long" + }, + "read_io": { + "properties": { + "ops_per_sec": { + "type": "float" + } + } + }, + "replica_lag": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "swap_usage": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "throughput": { + "properties": { + "commit": { + "type": "long" + }, + "ddl": { + "type": "long" + }, + "delete": { + "type": "long" + }, + "dml": { + "type": "long" + }, + "insert": { + "type": "long" + }, + "network": { + "type": "long" + }, + "network_receive": { + "type": "long" + }, + "network_transmit": { + "type": "long" + }, + "read": { + "type": "long" + }, + "select": { + "type": "long" + }, + "update": { + "type": "long" + }, + "write": { + "type": "long" + } + } + }, + "transaction_logs_generation": { + "type": "long" + }, + "transactions": { + "properties": { + "active": { + "type": "long" + }, + "blocked": { + "type": "long" + } + } + }, + "volume_used": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "write_io": { + "properties": { + "ops_per_sec": { + "type": "float" + } + } + } + } + }, + "s3_daily_storage": { + "properties": { + "bucket": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "number_of_objects": { + "type": "long" + } + } + }, + "s3_request": { + "properties": { + "bucket": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "downloaded": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "errors": { + "properties": { + "4xx": { + "type": "long" + }, + "5xx": { + "type": "long" + } + } + }, + "latency": { + "properties": { + "first_byte": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "total_request": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "requests": { + "properties": { + "delete": { + "type": "long" + }, + "get": { + "type": "long" + }, + "head": { + "type": "long" + }, + "list": { + "type": "long" + }, + "post": { + "type": "long" + }, + "put": { + "type": "long" + }, + "select": { + "type": "long" + }, + "select_returned": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "select_scanned": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "uploaded": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "sqs": { + "properties": { + "empty_receives": { + "type": "long" + }, + "messages": { + "properties": { + "delayed": { + "type": "long" + }, + "deleted": { + "type": "long" + }, + "not_visible": { + "type": "long" + }, + "received": { + "type": "long" + }, + "sent": { + "type": "long" + }, + "visible": { + "type": "long" + } + } + }, + "oldest_message_age": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "sent_message_size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "tags": { + "properties": { + "*": { + "type": "object" + }, + "Name": { + "type": "keyword" + } + } + } + } + }, + "beat": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "properties": { + "management": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "module": { + "properties": { + "count": { + "type": "long" + } + } + }, + "output": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "stats": { + "properties": { + "libbeat": { + "properties": { + "output": { + "properties": { + "events": { + "properties": { + "acked": { + "type": "long" + }, + "active": { + "type": "long" + }, + "batches": { + "type": "long" + }, + "dropped": { + "type": "long" + }, + "duplicates": { + "type": "long" + }, + "failed": { + "type": "long" + }, + "toomany": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "read": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "write": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + } + } + } + } + }, + "runtime": { + "properties": { + "goroutines": { + "type": "long" + } + } + }, + "uptime": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ceph": { + "properties": { + "cluster_disk": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "cluster_health": { + "properties": { + "overall_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "timechecks": { + "properties": { + "epoch": { + "type": "long" + }, + "round": { + "properties": { + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "type": "long" + } + } + } + } + } + } + }, + "cluster_status": { + "properties": { + "degraded": { + "properties": { + "objects": { + "type": "long" + }, + "ratio": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "total": { + "type": "long" + } + } + }, + "misplace": { + "properties": { + "objects": { + "type": "long" + }, + "ratio": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "total": { + "type": "long" + } + } + }, + "osd": { + "properties": { + "epoch": { + "type": "long" + }, + "full": { + "type": "boolean" + }, + "nearfull": { + "type": "boolean" + }, + "num_in_osds": { + "type": "long" + }, + "num_osds": { + "type": "long" + }, + "num_remapped_pgs": { + "type": "long" + }, + "num_up_osds": { + "type": "long" + } + } + }, + "pg": { + "properties": { + "avail_bytes": { + "type": "long" + }, + "data_bytes": { + "type": "long" + }, + "total_bytes": { + "type": "long" + }, + "used_bytes": { + "type": "long" + } + } + }, + "pg_state": { + "properties": { + "count": { + "type": "long" + }, + "state_name": { + "type": "long" + }, + "version": { + "type": "long" + } + } + }, + "traffic": { + "properties": { + "read_bytes": { + "type": "long" + }, + "read_op_per_sec": { + "type": "long" + }, + "write_bytes": { + "type": "long" + }, + "write_op_per_sec": { + "type": "long" + } + } + }, + "version": { + "type": "long" + } + } + }, + "monitor_health": { + "properties": { + "available": { + "properties": { + "kb": { + "type": "long" + }, + "pct": { + "type": "long" + } + } + }, + "health": { + "ignore_above": 1024, + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "store_stats": { + "properties": { + "last_updated": { + "type": "long" + }, + "log": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "misc": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "sst": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "kb": { + "type": "long" + } + } + }, + "used": { + "properties": { + "kb": { + "type": "long" + } + } + } + } + }, + "osd_df": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "device_class": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pg_num": { + "type": "long" + }, + "total": { + "properties": { + "byte": { + "type": "long" + } + } + }, + "used": { + "properties": { + "byte": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "osd_tree": { + "properties": { + "children": { + "ignore_above": 1024, + "type": "keyword" + }, + "crush_weight": { + "type": "float" + }, + "depth": { + "type": "long" + }, + "device_class": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "father": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "primary_affinity": { + "type": "float" + }, + "reweight": { + "type": "long" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "type_id": { + "type": "long" + } + } + }, + "pool_disk": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "stats": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "objects": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "kb": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "cockroachdb": { + "type": "object" + }, + "consul": { + "properties": { + "agent": { + "properties": { + "autopilot": { + "properties": { + "healthy": { + "type": "boolean" + } + } + }, + "runtime": { + "properties": { + "alloc": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "garbage_collector": { + "properties": { + "pause": { + "properties": { + "current": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "total": { + "properties": { + "ns": { + "type": "long" + } + } + } + } + }, + "runs": { + "type": "long" + } + } + }, + "goroutines": { + "type": "long" + }, + "heap_objects": { + "type": "long" + }, + "malloc_count": { + "type": "long" + }, + "sys": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "annotation_checksum/configmap": { + "type": "keyword" + }, + "annotation_checksum/health": { + "type": "keyword" + }, + "annotation_checksum/secret": { + "type": "keyword" + }, + "annotation_configchecksum": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_hash": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_ports": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_restartCount": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_terminationMessagePath": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_terminationMessagePolicy": { + "type": "keyword" + }, + "annotation_io_kubernetes_pod_terminationGracePeriod": { + "type": "keyword" + }, + "annotation_kubernetes_io/config_hash": { + "type": "keyword" + }, + "annotation_kubernetes_io/config_seen": { + "type": "keyword" + }, + "annotation_kubernetes_io/config_source": { + "type": "keyword" + }, + "annotation_kubernetes_io/limit-ranger": { + "type": "keyword" + }, + "annotation_scheduler_alpha_kubernetes_io/critical-pod": { + "type": "keyword" + }, + "annotation_seccomp_security_alpha_kubernetes_io/pod": { + "type": "keyword" + }, + "app": { + "type": "keyword" + }, + "chart": { + "type": "keyword" + }, + "component": { + "type": "keyword" + }, + "controller-revision-hash": { + "type": "keyword" + }, + "controller-uid": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "heritage": { + "type": "keyword" + }, + "io_kubernetes_container_logpath": { + "type": "keyword" + }, + "io_kubernetes_container_name": { + "type": "keyword" + }, + "io_kubernetes_docker_type": { + "type": "keyword" + }, + "io_kubernetes_pod_name": { + "type": "keyword" + }, + "io_kubernetes_pod_namespace": { + "type": "keyword" + }, + "io_kubernetes_pod_uid": { + "type": "keyword" + }, + "io_kubernetes_sandbox_id": { + "type": "keyword" + }, + "job-name": { + "type": "keyword" + }, + "k8s-app": { + "type": "keyword" + }, + "kubernetes_io/cluster-service": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "maintainer": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "org_label-schema_build-date": { + "type": "keyword" + }, + "org_label-schema_license": { + "type": "keyword" + }, + "org_label-schema_name": { + "type": "keyword" + }, + "org_label-schema_schema-version": { + "type": "keyword" + }, + "org_label-schema_url": { + "type": "keyword" + }, + "org_label-schema_vcs-ref": { + "type": "keyword" + }, + "org_label-schema_vcs-url": { + "type": "keyword" + }, + "org_label-schema_vendor": { + "type": "keyword" + }, + "org_label-schema_version": { + "type": "keyword" + }, + "pod-template-generation": { + "type": "keyword" + }, + "pod-template-hash": { + "type": "keyword" + }, + "release": { + "type": "keyword" + }, + "role": { + "type": "keyword" + }, + "service": { + "type": "keyword" + }, + "statefulset_kubernetes_io/pod-name": { + "type": "keyword" + }, + "tier": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "coredns": { + "properties": { + "stats": { + "properties": { + "dns": { + "properties": { + "cache": { + "properties": { + "hits": { + "properties": { + "count": { + "type": "long" + } + } + }, + "misses": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "request": { + "properties": { + "count": { + "type": "long" + }, + "do": { + "properties": { + "count": { + "type": "long" + } + } + }, + "duration": { + "properties": { + "ns": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + }, + "size": { + "properties": { + "bytes": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + }, + "type": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "response": { + "properties": { + "rcode": { + "properties": { + "count": { + "type": "long" + } + } + }, + "size": { + "properties": { + "bytes": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "panic": { + "properties": { + "count": { + "type": "long" + } + } + }, + "proto": { + "ignore_above": 1024, + "type": "keyword" + }, + "rcode": { + "ignore_above": 1024, + "type": "keyword" + }, + "server": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "couchbase": { + "properties": { + "bucket": { + "properties": { + "data": { + "properties": { + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "disk": { + "properties": { + "fetches": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "item_count": { + "type": "long" + }, + "memory": { + "properties": { + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ops_per_sec": { + "type": "long" + }, + "quota": { + "properties": { + "ram": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "use": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "cluster": { + "properties": { + "hdd": { + "properties": { + "free": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "quota": { + "properties": { + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "by_data": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "value": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "max_bucket_count": { + "type": "long" + }, + "quota": { + "properties": { + "index_memory": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "memory": { + "properties": { + "mb": { + "type": "long" + } + } + } + } + }, + "ram": { + "properties": { + "quota": { + "properties": { + "total": { + "properties": { + "per_node": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "value": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "used": { + "properties": { + "per_node": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "value": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "by_data": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "value": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "node": { + "properties": { + "cmd_get": { + "type": "long" + }, + "couch": { + "properties": { + "docs": { + "properties": { + "data_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "disk_size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "spatial": { + "properties": { + "data_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "disk_size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "views": { + "properties": { + "data_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "disk_size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "cpu_utilization_rate": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "current_items": { + "properties": { + "total": { + "type": "long" + }, + "value": { + "type": "long" + } + } + }, + "ep_bg_fetched": { + "type": "long" + }, + "get_hits": { + "type": "long" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "mcd_memory": { + "properties": { + "allocated": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "reserved": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "free": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "ops": { + "type": "long" + }, + "swap": { + "properties": { + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "uptime": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "vb_replica_curr_items": { + "type": "long" + } + } + } + } + }, + "couchdb": { + "properties": { + "server": { + "properties": { + "couchdb": { + "properties": { + "auth_cache_hits": { + "type": "long" + }, + "auth_cache_misses": { + "type": "long" + }, + "database_reads": { + "type": "long" + }, + "database_writes": { + "type": "long" + }, + "open_databases": { + "type": "long" + }, + "open_os_files": { + "type": "long" + }, + "request_time": { + "type": "long" + } + } + }, + "httpd": { + "properties": { + "bulk_requests": { + "type": "long" + }, + "clients_requesting_changes": { + "type": "long" + }, + "requests": { + "type": "long" + }, + "temporary_view_reads": { + "type": "long" + }, + "view_reads": { + "type": "long" + } + } + }, + "httpd_request_methods": { + "properties": { + "COPY": { + "type": "long" + }, + "DELETE": { + "type": "long" + }, + "GET": { + "type": "long" + }, + "HEAD": { + "type": "long" + }, + "POST": { + "type": "long" + }, + "PUT": { + "type": "long" + } + } + }, + "httpd_status_codes": { + "properties": { + "200": { + "type": "long" + }, + "201": { + "type": "long" + }, + "202": { + "type": "long" + }, + "301": { + "type": "long" + }, + "304": { + "type": "long" + }, + "400": { + "type": "long" + }, + "401": { + "type": "long" + }, + "403": { + "type": "long" + }, + "404": { + "type": "long" + }, + "405": { + "type": "long" + }, + "409": { + "type": "long" + }, + "412": { + "type": "long" + }, + "500": { + "type": "long" + } + } + } + } + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "command": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "ip_addresses": { + "type": "ip" + }, + "labels": { + "properties": { + "annotation_checksum/configmap": { + "type": "keyword" + }, + "annotation_checksum/health": { + "type": "keyword" + }, + "annotation_checksum/secret": { + "type": "keyword" + }, + "annotation_configchecksum": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_hash": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_ports": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_restartCount": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_terminationMessagePath": { + "type": "keyword" + }, + "annotation_io_kubernetes_container_terminationMessagePolicy": { + "type": "keyword" + }, + "annotation_io_kubernetes_pod_terminationGracePeriod": { + "type": "keyword" + }, + "annotation_kubernetes_io/config_hash": { + "type": "keyword" + }, + "annotation_kubernetes_io/config_seen": { + "type": "keyword" + }, + "annotation_kubernetes_io/config_source": { + "type": "keyword" + }, + "annotation_kubernetes_io/limit-ranger": { + "type": "keyword" + }, + "annotation_scheduler_alpha_kubernetes_io/critical-pod": { + "type": "keyword" + }, + "annotation_seccomp_security_alpha_kubernetes_io/pod": { + "type": "keyword" + }, + "app": { + "type": "keyword" + }, + "chart": { + "type": "keyword" + }, + "component": { + "type": "keyword" + }, + "controller-revision-hash": { + "type": "keyword" + }, + "controller-uid": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "heritage": { + "type": "keyword" + }, + "io_kubernetes_container_logpath": { + "type": "keyword" + }, + "io_kubernetes_container_name": { + "type": "keyword" + }, + "io_kubernetes_docker_type": { + "type": "keyword" + }, + "io_kubernetes_pod_name": { + "type": "keyword" + }, + "io_kubernetes_pod_namespace": { + "type": "keyword" + }, + "io_kubernetes_pod_uid": { + "type": "keyword" + }, + "io_kubernetes_sandbox_id": { + "type": "keyword" + }, + "job-name": { + "type": "keyword" + }, + "k8s-app": { + "type": "keyword" + }, + "kubernetes_io/cluster-service": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "maintainer": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "org_label-schema_build-date": { + "type": "keyword" + }, + "org_label-schema_license": { + "type": "keyword" + }, + "org_label-schema_name": { + "type": "keyword" + }, + "org_label-schema_schema-version": { + "type": "keyword" + }, + "org_label-schema_url": { + "type": "keyword" + }, + "org_label-schema_vcs-ref": { + "type": "keyword" + }, + "org_label-schema_vcs-url": { + "type": "keyword" + }, + "org_label-schema_vendor": { + "type": "keyword" + }, + "org_label-schema_version": { + "type": "keyword" + }, + "pod-template-generation": { + "type": "keyword" + }, + "pod-template-hash": { + "type": "keyword" + }, + "release": { + "type": "keyword" + }, + "role": { + "type": "keyword" + }, + "service": { + "type": "keyword" + }, + "statefulset_kubernetes_io/pod-name": { + "type": "keyword" + }, + "tier": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "size": { + "properties": { + "root_fs": { + "type": "long" + }, + "rw": { + "type": "long" + } + } + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "cpu": { + "properties": { + "core": { + "properties": { + "*": { + "properties": { + "pct": { + "type": "object" + }, + "ticks": { + "type": "object" + } + } + }, + "0": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "1": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "2": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "3": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + } + } + }, + "kernel": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "system": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "total": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "user": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + } + } + }, + "diskio": { + "properties": { + "read": { + "properties": { + "bytes": { + "type": "long" + }, + "ops": { + "type": "long" + }, + "rate": { + "type": "long" + } + } + }, + "reads": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "summary": { + "properties": { + "bytes": { + "type": "long" + }, + "ops": { + "type": "long" + }, + "rate": { + "type": "long" + } + } + }, + "total": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "write": { + "properties": { + "bytes": { + "type": "long" + }, + "ops": { + "type": "long" + }, + "rate": { + "type": "long" + } + } + }, + "writes": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "actor": { + "properties": { + "attributes": { + "type": "object" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "from": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "healthcheck": { + "properties": { + "event": { + "properties": { + "end_date": { + "type": "date" + }, + "exit_code": { + "type": "long" + }, + "output": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_date": { + "type": "date" + } + } + }, + "failingstreak": { + "type": "long" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "image": { + "properties": { + "created": { + "type": "date" + }, + "id": { + "properties": { + "current": { + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "size": { + "properties": { + "regular": { + "type": "long" + }, + "virtual": { + "type": "long" + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "info": { + "properties": { + "containers": { + "properties": { + "paused": { + "type": "long" + }, + "running": { + "type": "long" + }, + "stopped": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "images": { + "type": "long" + } + } + }, + "memory": { + "properties": { + "commit": { + "properties": { + "peak": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "fail": { + "properties": { + "count": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "limit": { + "type": "long" + }, + "private_working_set": { + "properties": { + "total": { + "type": "long" + } + } + }, + "rss": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "total": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "max": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "total": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "in": { + "properties": { + "bytes": { + "type": "long" + }, + "dropped": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "errors": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + }, + "inbound": { + "properties": { + "bytes": { + "type": "long" + }, + "dropped": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + }, + "interface": { + "ignore_above": 1024, + "type": "keyword" + }, + "out": { + "properties": { + "bytes": { + "type": "long" + }, + "dropped": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "errors": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + }, + "outbound": { + "properties": { + "bytes": { + "type": "long" + }, + "dropped": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + } + } + } + } + }, + "dropwizard": { + "type": "object" + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "elasticsearch": { + "properties": { + "ccr": { + "properties": { + "follower": { + "properties": { + "global_checkpoint": { + "type": "long" + }, + "index": { + "ignore_above": 1024, + "type": "keyword" + }, + "operations_written": { + "type": "long" + }, + "shard": { + "properties": { + "number": { + "type": "long" + } + } + }, + "time_since_last_read": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "leader": { + "properties": { + "index": { + "ignore_above": 1024, + "type": "keyword" + }, + "max_seq_no": { + "type": "long" + } + } + } + } + }, + "cluster": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pending_task": { + "properties": { + "insert_order": { + "type": "long" + }, + "priority": { + "type": "long" + }, + "source": { + "ignore_above": 1024, + "type": "keyword" + }, + "time_in_queue": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "state": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "stats": { + "properties": { + "indices": { + "properties": { + "count": { + "type": "long" + }, + "fielddata": { + "properties": { + "memory": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "shards": { + "properties": { + "count": { + "type": "long" + }, + "primaries": { + "type": "long" + } + } + } + } + }, + "nodes": { + "properties": { + "count": { + "type": "long" + }, + "data": { + "type": "long" + }, + "master": { + "type": "long" + } + } + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "index": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "recovery": { + "properties": { + "id": { + "type": "long" + }, + "primary": { + "type": "boolean" + }, + "source": { + "properties": { + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "stage": { + "ignore_above": 1024, + "type": "keyword" + }, + "target": { + "properties": { + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "summary": { + "properties": { + "primaries": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + }, + "deleted": { + "type": "long" + } + } + }, + "segments": { + "properties": { + "count": { + "type": "long" + }, + "memory": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "store": { + "properties": { + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + }, + "deleted": { + "type": "long" + } + } + }, + "segments": { + "properties": { + "count": { + "type": "long" + }, + "memory": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "store": { + "properties": { + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + }, + "deleted": { + "type": "long" + } + } + }, + "segments": { + "properties": { + "count": { + "type": "long" + }, + "memory": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "store": { + "properties": { + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "ml": { + "properties": { + "job": { + "properties": { + "data_counts": { + "properties": { + "invalid_date_count": { + "type": "long" + }, + "processed_record_count": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "node": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "jvm": { + "properties": { + "memory": { + "properties": { + "heap": { + "properties": { + "init": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "nonheap": { + "properties": { + "init": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "process": { + "properties": { + "mlockall": { + "type": "boolean" + } + } + }, + "stats": { + "properties": { + "fs": { + "properties": { + "summary": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "free": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "indices": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + }, + "deleted": { + "type": "long" + } + } + }, + "segments": { + "properties": { + "count": { + "type": "long" + }, + "memory": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "store": { + "properties": { + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "jvm": { + "properties": { + "gc": { + "properties": { + "collectors": { + "properties": { + "old": { + "properties": { + "collection": { + "properties": { + "count": { + "type": "long" + }, + "ms": { + "type": "long" + } + } + } + } + }, + "young": { + "properties": { + "collection": { + "properties": { + "count": { + "type": "long" + }, + "ms": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "mem": { + "properties": { + "pools": { + "properties": { + "old": { + "properties": { + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "peak": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "peak_max": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "survivor": { + "properties": { + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "peak": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "peak_max": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "young": { + "properties": { + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "peak": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "peak_max": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "shard": { + "properties": { + "number": { + "type": "long" + }, + "primary": { + "type": "boolean" + }, + "relocating_node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "envoyproxy": { + "properties": { + "server": { + "properties": { + "cluster_manager": { + "properties": { + "active_clusters": { + "type": "long" + }, + "cluster_added": { + "type": "long" + }, + "cluster_modified": { + "type": "long" + }, + "cluster_removed": { + "type": "long" + }, + "warming_clusters": { + "type": "long" + } + } + }, + "filesystem": { + "properties": { + "flushed_by_timer": { + "type": "long" + }, + "reopen_failed": { + "type": "long" + }, + "write_buffered": { + "type": "long" + }, + "write_completed": { + "type": "long" + }, + "write_total_buffered": { + "type": "long" + } + } + }, + "http2": { + "properties": { + "header_overflow": { + "type": "long" + }, + "headers_cb_no_stream": { + "type": "long" + }, + "rx_messaging_error": { + "type": "long" + }, + "rx_reset": { + "type": "long" + }, + "too_many_header_frames": { + "type": "long" + }, + "trailers": { + "type": "long" + }, + "tx_reset": { + "type": "long" + } + } + }, + "listener_manager": { + "properties": { + "listener_added": { + "type": "long" + }, + "listener_create_failure": { + "type": "long" + }, + "listener_create_success": { + "type": "long" + }, + "listener_modified": { + "type": "long" + }, + "listener_removed": { + "type": "long" + }, + "total_listeners_active": { + "type": "long" + }, + "total_listeners_draining": { + "type": "long" + }, + "total_listeners_warming": { + "type": "long" + } + } + }, + "runtime": { + "properties": { + "admin_overrides_active": { + "type": "long" + }, + "load_error": { + "type": "long" + }, + "load_success": { + "type": "long" + }, + "num_keys": { + "type": "long" + }, + "override_dir_exists": { + "type": "long" + }, + "override_dir_not_exists": { + "type": "long" + } + } + }, + "server": { + "properties": { + "days_until_first_cert_expiring": { + "type": "long" + }, + "hot_restart_epoch": { + "type": "long" + }, + "live": { + "type": "long" + }, + "memory_allocated": { + "type": "long" + }, + "memory_heap_size": { + "type": "long" + }, + "parent_connections": { + "type": "long" + }, + "total_connections": { + "type": "long" + }, + "uptime": { + "type": "long" + }, + "version": { + "type": "long" + }, + "watchdog_mega_miss": { + "type": "long" + }, + "watchdog_miss": { + "type": "long" + } + } + }, + "stats": { + "properties": { + "overflow": { + "type": "long" + } + } + } + } + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "etcd": { + "properties": { + "api_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "disk": { + "properties": { + "backend_commit_duration": { + "properties": { + "ns": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + }, + "mvcc_db_total_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "wal_fsync_duration": { + "properties": { + "ns": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + } + } + }, + "leader": { + "properties": { + "followers": { + "properties": { + "counts": { + "properties": { + "followers": { + "properties": { + "counts": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + } + } + } + } + }, + "latency": { + "properties": { + "follower": { + "properties": { + "latency": { + "properties": { + "standardDeviation": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "followers": { + "properties": { + "latency": { + "properties": { + "average": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "current": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "maximum": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "minimum": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "leader": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory": { + "properties": { + "go_memstats_alloc": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "client_grpc_received": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "client_grpc_sent": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "self": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "leaderinfo": { + "properties": { + "leader": { + "ignore_above": 1024, + "type": "keyword" + }, + "starttime": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "recv": { + "properties": { + "appendrequest": { + "properties": { + "count": { + "type": "long" + } + } + }, + "bandwidthrate": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "pkgrate": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "send": { + "properties": { + "appendrequest": { + "properties": { + "count": { + "type": "long" + } + } + }, + "bandwidthrate": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "pkgrate": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "starttime": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "grpc_handled": { + "properties": { + "count": { + "type": "long" + } + } + }, + "grpc_started": { + "properties": { + "count": { + "type": "long" + } + } + }, + "has_leader": { + "type": "byte" + }, + "leader_changes": { + "properties": { + "count": { + "type": "long" + } + } + }, + "proposals_committed": { + "properties": { + "count": { + "type": "long" + } + } + }, + "proposals_failed": { + "properties": { + "count": { + "type": "long" + } + } + }, + "proposals_pending": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "store": { + "properties": { + "compareanddelete": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + }, + "compareandswap": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + }, + "create": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + }, + "delete": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + }, + "expire": { + "properties": { + "count": { + "type": "long" + } + } + }, + "gets": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + }, + "sets": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + }, + "update": { + "properties": { + "fail": { + "type": "long" + }, + "success": { + "type": "long" + } + } + }, + "watchers": { + "type": "long" + } + } + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "target_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "golang": { + "properties": { + "expvar": { + "properties": { + "cmdline": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "heap": { + "properties": { + "allocations": { + "properties": { + "active": { + "type": "long" + }, + "allocated": { + "type": "long" + }, + "frees": { + "type": "long" + }, + "idle": { + "type": "long" + }, + "mallocs": { + "type": "long" + }, + "objects": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "cmdline": { + "ignore_above": 1024, + "type": "keyword" + }, + "gc": { + "properties": { + "cpu_fraction": { + "type": "float" + }, + "next_gc_limit": { + "type": "long" + }, + "pause": { + "properties": { + "avg": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "count": { + "type": "long" + }, + "max": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "sum": { + "properties": { + "ns": { + "type": "long" + } + } + } + } + }, + "total_count": { + "type": "long" + }, + "total_pause": { + "properties": { + "ns": { + "type": "long" + } + } + } + } + }, + "system": { + "properties": { + "obtained": { + "type": "long" + }, + "released": { + "type": "long" + }, + "stack": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + } + } + }, + "graphite": { + "properties": { + "server": { + "properties": { + "example": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "haproxy": { + "properties": { + "info": { + "properties": { + "compress": { + "properties": { + "bps": { + "properties": { + "in": { + "type": "long" + }, + "out": { + "type": "long" + }, + "rate_limit": { + "type": "long" + } + } + } + } + }, + "connection": { + "properties": { + "current": { + "type": "long" + }, + "hard_max": { + "type": "long" + }, + "max": { + "type": "long" + }, + "rate": { + "properties": { + "limit": { + "type": "long" + }, + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + }, + "ssl": { + "properties": { + "current": { + "type": "long" + }, + "max": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "idle": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "memory": { + "properties": { + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "pipes": { + "properties": { + "free": { + "type": "long" + }, + "max": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "process_num": { + "type": "long" + }, + "processes": { + "type": "long" + }, + "requests": { + "properties": { + "max": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "run_queue": { + "type": "long" + }, + "session": { + "properties": { + "rate": { + "properties": { + "limit": { + "type": "long" + }, + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + } + } + }, + "sockets": { + "properties": { + "max": { + "type": "long" + } + } + }, + "ssl": { + "properties": { + "backend": { + "properties": { + "key_rate": { + "properties": { + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + } + } + }, + "cache_misses": { + "type": "long" + }, + "cached_lookups": { + "type": "long" + }, + "frontend": { + "properties": { + "key_rate": { + "properties": { + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + }, + "session_reuse": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "rate": { + "properties": { + "limit": { + "type": "long" + }, + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + } + } + }, + "tasks": { + "type": "long" + }, + "ulimit_n": { + "type": "long" + }, + "uptime": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "zlib_mem_usage": { + "properties": { + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + } + } + }, + "stat": { + "properties": { + "check": { + "properties": { + "agent": { + "properties": { + "last": { + "type": "long" + } + } + }, + "code": { + "type": "long" + }, + "down": { + "type": "long" + }, + "duration": { + "type": "long" + }, + "failed": { + "type": "long" + }, + "health": { + "properties": { + "fail": { + "type": "long" + }, + "last": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "properties": { + "aborted": { + "type": "long" + } + } + }, + "component_type": { + "type": "long" + }, + "compressor": { + "properties": { + "bypassed": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "in": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "out": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "response": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "connection": { + "properties": { + "retried": { + "type": "long" + }, + "time": { + "properties": { + "avg": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "downtime": { + "type": "long" + }, + "in": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "last_change": { + "type": "long" + }, + "out": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "proxy": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "queue": { + "properties": { + "limit": { + "type": "long" + }, + "time": { + "properties": { + "avg": { + "type": "long" + } + } + } + } + }, + "request": { + "properties": { + "connection": { + "properties": { + "errors": { + "type": "long" + } + } + }, + "denied": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "queued": { + "properties": { + "current": { + "type": "long" + }, + "max": { + "type": "long" + } + } + }, + "rate": { + "properties": { + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + }, + "redispatched": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "response": { + "properties": { + "denied": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "http": { + "properties": { + "1xx": { + "type": "long" + }, + "2xx": { + "type": "long" + }, + "3xx": { + "type": "long" + }, + "4xx": { + "type": "long" + }, + "5xx": { + "type": "long" + }, + "other": { + "type": "long" + } + } + }, + "time": { + "properties": { + "avg": { + "type": "long" + } + } + } + } + }, + "selected": { + "properties": { + "total": { + "type": "long" + } + } + }, + "server": { + "properties": { + "aborted": { + "type": "long" + }, + "active": { + "type": "long" + }, + "backup": { + "type": "long" + }, + "id": { + "type": "long" + } + } + }, + "service_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "session": { + "properties": { + "current": { + "type": "long" + }, + "limit": { + "type": "long" + }, + "max": { + "type": "long" + }, + "rate": { + "properties": { + "limit": { + "type": "long" + }, + "max": { + "type": "long" + }, + "value": { + "type": "long" + } + } + } + } + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "throttle": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "tracked": { + "properties": { + "id": { + "type": "long" + } + } + }, + "weight": { + "type": "long" + } + } + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "json": { + "type": "object" + }, + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "headers": { + "type": "object" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "headers": { + "type": "object" + }, + "phrase": { + "ignore_above": 1024, + "type": "keyword" + }, + "status_code": { + "type": "long" + } + } + }, + "server": { + "type": "object" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kafka": { + "properties": { + "broker": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + } + } + }, + "consumergroup": { + "properties": { + "broker": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + } + } + }, + "client": { + "properties": { + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "member_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "meta": { + "ignore_above": 1024, + "type": "keyword" + }, + "offset": { + "type": "long" + }, + "partition": { + "type": "long" + }, + "topic": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "partition": { + "properties": { + "broker": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + } + } + }, + "id": { + "type": "long" + }, + "offset": { + "properties": { + "newest": { + "type": "long" + }, + "oldest": { + "type": "long" + } + } + }, + "partition": { + "properties": { + "error": { + "properties": { + "code": { + "type": "long" + } + } + }, + "id": { + "type": "long" + }, + "insync_replica": { + "type": "boolean" + }, + "is_leader": { + "type": "boolean" + }, + "isr": { + "ignore_above": 1024, + "type": "keyword" + }, + "leader": { + "type": "long" + }, + "replica": { + "type": "long" + } + } + }, + "topic": { + "properties": { + "error": { + "properties": { + "code": { + "type": "long" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "topic_broker_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "topic_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "topic": { + "properties": { + "error": { + "properties": { + "code": { + "type": "long" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "kibana": { + "properties": { + "stats": { + "properties": { + "concurrent_connections": { + "type": "long" + }, + "host": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "index": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "process": { + "properties": { + "event_loop_delay": { + "properties": { + "ms": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "memory": { + "properties": { + "heap": { + "properties": { + "size_limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "uptime": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "request": { + "properties": { + "disconnects": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "response_time": { + "properties": { + "avg": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "max": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "snapshot": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "status": { + "properties": { + "metrics": { + "properties": { + "concurrent_connections": { + "type": "long" + }, + "requests": { + "properties": { + "disconnects": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "properties": { + "overall": { + "properties": { + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "type": "object" + }, + "apiserver": { + "properties": { + "audit": { + "properties": { + "event": { + "properties": { + "count": { + "type": "long" + } + } + }, + "rejected": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "client": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "etcd": { + "properties": { + "object": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + }, + "duration": { + "properties": { + "us": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "double" + } + } + } + } + }, + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + }, + "response": { + "properties": { + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "process": { + "properties": { + "cpu": { + "properties": { + "sec": { + "type": "double" + } + } + }, + "fds": { + "properties": { + "open": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "resident": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "virtual": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "started": { + "properties": { + "sec": { + "type": "double" + } + } + } + } + }, + "request": { + "properties": { + "client": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "component": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "count": { + "type": "long" + }, + "current": { + "properties": { + "count": { + "type": "long" + } + } + }, + "dry_run": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handler": { + "ignore_above": 1024, + "type": "keyword" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "latency": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + }, + "longrunning": { + "properties": { + "count": { + "type": "long" + } + } + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource": { + "ignore_above": 1024, + "type": "keyword" + }, + "scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "subresource": { + "ignore_above": 1024, + "type": "keyword" + }, + "verb": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "container": { + "properties": { + "_module": { + "properties": { + "labels": { + "properties": { + "app": { + "ignore_above": 1024, + "type": "keyword" + }, + "chart": { + "ignore_above": 1024, + "type": "keyword" + }, + "component": { + "ignore_above": 1024, + "type": "keyword" + }, + "controller-revision-hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "controller-uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "heritage": { + "ignore_above": 1024, + "type": "keyword" + }, + "job-name": { + "ignore_above": 1024, + "type": "keyword" + }, + "k8s-app": { + "ignore_above": 1024, + "type": "keyword" + }, + "kubernetes": { + "properties": { + "io/cluster-service": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pod-template-generation": { + "ignore_above": 1024, + "type": "keyword" + }, + "pod-template-hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "release": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "statefulset": { + "properties": { + "kubernetes": { + "properties": { + "io/pod-name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tier": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cpu": { + "properties": { + "limit": { + "properties": { + "cores": { + "type": "float" + }, + "nanocores": { + "type": "long" + } + } + }, + "request": { + "properties": { + "cores": { + "type": "float" + }, + "nanocores": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "core": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "limit": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "nanocores": { + "type": "long" + }, + "node": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "logs": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "capacity": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inodes": { + "properties": { + "count": { + "type": "long" + }, + "free": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "majorpagefaults": { + "type": "long" + }, + "pagefaults": { + "type": "long" + }, + "request": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "limit": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "node": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "workingset": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "rootfs": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "capacity": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inodes": { + "properties": { + "used": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "start_time": { + "type": "date" + }, + "status": { + "properties": { + "phase": { + "ignore_above": 1024, + "type": "keyword" + }, + "ready": { + "type": "boolean" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "restarts": { + "type": "long" + } + } + } + } + }, + "controllermanager": { + "properties": { + "client": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handler": { + "ignore_above": 1024, + "type": "keyword" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "http": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + }, + "duration": { + "properties": { + "us": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "double" + } + } + } + } + }, + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + }, + "response": { + "properties": { + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "leader": { + "properties": { + "is_master": { + "type": "boolean" + } + } + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "collector": { + "properties": { + "count": { + "type": "long" + }, + "eviction": { + "properties": { + "count": { + "type": "long" + } + } + }, + "health": { + "properties": { + "pct": { + "type": "long" + } + } + }, + "unhealthy": { + "properties": { + "count": { + "type": "long" + } + } + } + } + } + } + }, + "process": { + "properties": { + "cpu": { + "properties": { + "sec": { + "type": "double" + } + } + }, + "fds": { + "properties": { + "open": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "resident": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "virtual": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "started": { + "properties": { + "sec": { + "type": "double" + } + } + } + } + }, + "workqueue": { + "properties": { + "adds": { + "properties": { + "count": { + "type": "long" + } + } + }, + "depth": { + "properties": { + "count": { + "type": "long" + } + } + }, + "longestrunning": { + "properties": { + "sec": { + "type": "double" + } + } + }, + "retries": { + "properties": { + "count": { + "type": "long" + } + } + }, + "unfinished": { + "properties": { + "sec": { + "type": "double" + } + } + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "paused": { + "type": "boolean" + }, + "replicas": { + "properties": { + "available": { + "type": "long" + }, + "desired": { + "type": "long" + }, + "unavailable": { + "type": "long" + }, + "updated": { + "type": "long" + } + } + } + } + }, + "event": { + "properties": { + "count": { + "type": "long" + }, + "involved_object": { + "properties": { + "api_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "copy_to": [ + "message" + ], + "ignore_above": 1024, + "type": "keyword" + }, + "metadata": { + "properties": { + "generate_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "self_link": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "properties": { + "created": { + "type": "date" + } + } + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "properties": { + "first_occurrence": { + "type": "date" + }, + "last_occurrence": { + "type": "date" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "addonmanager": { + "properties": { + "kubernetes": { + "properties": { + "io/mode": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "app": { + "ignore_above": 1024, + "type": "keyword" + }, + "chart": { + "ignore_above": 1024, + "type": "keyword" + }, + "component": { + "ignore_above": 1024, + "type": "keyword" + }, + "controller-revision-hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "controller-uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "heritage": { + "ignore_above": 1024, + "type": "keyword" + }, + "job-name": { + "ignore_above": 1024, + "type": "keyword" + }, + "k8s-app": { + "ignore_above": 1024, + "type": "keyword" + }, + "kubernetes": { + "properties": { + "io/cluster-service": { + "ignore_above": 1024, + "type": "keyword" + }, + "io/name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pod-template-generation": { + "ignore_above": 1024, + "type": "keyword" + }, + "pod-template-hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "release": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "statefulset": { + "properties": { + "kubernetes": { + "properties": { + "io/pod-name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "statefulset_kubernetes_io/pod-name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tier": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "_module": { + "properties": { + "labels": { + "properties": { + "beta": { + "properties": { + "kubernetes": { + "properties": { + "io/arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "io/fluentd-ds-ready": { + "ignore_above": 1024, + "type": "keyword" + }, + "io/instance-type": { + "ignore_above": 1024, + "type": "keyword" + }, + "io/os": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "google": { + "properties": { + "com/gke-nodepool": { + "ignore_above": 1024, + "type": "keyword" + }, + "com/gke-os-distribution": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "failure-domain": { + "properties": { + "beta": { + "properties": { + "kubernetes": { + "properties": { + "io/region": { + "ignore_above": 1024, + "type": "keyword" + }, + "io/zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "kubernetes": { + "properties": { + "io/hostname": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "cpu": { + "properties": { + "allocatable": { + "properties": { + "cores": { + "type": "float" + } + } + }, + "capacity": { + "properties": { + "cores": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "core": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "nanocores": { + "type": "long" + } + } + } + } + }, + "fs": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "capacity": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inodes": { + "properties": { + "count": { + "type": "long" + }, + "free": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "allocatable": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "capacity": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "majorpagefaults": { + "type": "long" + }, + "pagefaults": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "workingset": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "network": { + "properties": { + "rx": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + }, + "tx": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + } + } + }, + "pod": { + "properties": { + "allocatable": { + "properties": { + "total": { + "type": "long" + } + } + }, + "capacity": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "runtime": { + "properties": { + "imagefs": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "capacity": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "start_time": { + "type": "date" + }, + "status": { + "properties": { + "ready": { + "ignore_above": 1024, + "type": "keyword" + }, + "unschedulable": { + "type": "boolean" + } + } + } + } + }, + "pod": { + "properties": { + "_module": { + "properties": { + "labels": { + "properties": { + "app": { + "ignore_above": 1024, + "type": "keyword" + }, + "chart": { + "ignore_above": 1024, + "type": "keyword" + }, + "component": { + "ignore_above": 1024, + "type": "keyword" + }, + "controller-revision-hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "controller-uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "heritage": { + "ignore_above": 1024, + "type": "keyword" + }, + "job-name": { + "ignore_above": 1024, + "type": "keyword" + }, + "k8s-app": { + "ignore_above": 1024, + "type": "keyword" + }, + "kubernetes": { + "properties": { + "io/cluster-service": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pod-template-generation": { + "ignore_above": 1024, + "type": "keyword" + }, + "pod-template-hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "release": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "statefulset": { + "properties": { + "kubernetes": { + "properties": { + "io/pod-name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tier": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cpu": { + "properties": { + "usage": { + "properties": { + "limit": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "nanocores": { + "type": "long" + }, + "node": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "host_ip": { + "type": "ip" + }, + "ip": { + "type": "ip" + }, + "memory": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "major_page_faults": { + "type": "long" + }, + "page_faults": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "limit": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "node": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "working_set": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "network": { + "properties": { + "rx": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + }, + "tx": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + } + } + }, + "start_time": { + "type": "date" + }, + "status": { + "properties": { + "phase": { + "ignore_above": 1024, + "type": "keyword" + }, + "ready": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheduled": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "proxy": { + "properties": { + "client": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handler": { + "ignore_above": 1024, + "type": "keyword" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "http": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + }, + "duration": { + "properties": { + "us": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "double" + } + } + } + } + }, + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + }, + "response": { + "properties": { + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "process": { + "properties": { + "cpu": { + "properties": { + "sec": { + "type": "double" + } + } + }, + "fds": { + "properties": { + "open": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "resident": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "virtual": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "started": { + "properties": { + "sec": { + "type": "double" + } + } + } + } + }, + "sync": { + "properties": { + "networkprogramming": { + "properties": { + "duration": { + "properties": { + "us": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + } + } + }, + "rules": { + "properties": { + "duration": { + "properties": { + "us": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + } + } + } + } + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "replicas": { + "properties": { + "available": { + "type": "long" + }, + "desired": { + "type": "long" + }, + "labeled": { + "type": "long" + }, + "observed": { + "type": "long" + }, + "ready": { + "type": "long" + } + } + } + } + }, + "scheduler": { + "properties": { + "client": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handler": { + "ignore_above": 1024, + "type": "keyword" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "http": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + }, + "duration": { + "properties": { + "us": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "double" + } + } + } + } + }, + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + }, + "response": { + "properties": { + "size": { + "properties": { + "bytes": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "leader": { + "properties": { + "is_master": { + "type": "boolean" + } + } + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "operation": { + "ignore_above": 1024, + "type": "keyword" + }, + "process": { + "properties": { + "cpu": { + "properties": { + "sec": { + "type": "double" + } + } + }, + "fds": { + "properties": { + "open": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "resident": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "virtual": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "started": { + "properties": { + "sec": { + "type": "double" + } + } + } + } + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheduling": { + "properties": { + "duration": { + "properties": { + "seconds": { + "properties": { + "count": { + "type": "long" + }, + "percentile": { + "properties": { + "*": { + "type": "object" + } + } + }, + "sum": { + "type": "double" + } + } + } + } + }, + "e2e": { + "properties": { + "duration": { + "properties": { + "us": { + "properties": { + "bucket": { + "properties": { + "*": { + "type": "object" + } + } + }, + "count": { + "type": "long" + }, + "sum": { + "type": "long" + } + } + } + } + } + } + }, + "pod": { + "properties": { + "attempts": { + "properties": { + "count": { + "type": "long" + } + } + }, + "preemption": { + "properties": { + "victims": { + "properties": { + "count": { + "type": "long" + } + } + } + } + } + } + } + } + } + } + }, + "statefulset": { + "properties": { + "created": { + "type": "long" + }, + "generation": { + "properties": { + "desired": { + "type": "long" + }, + "observed": { + "type": "long" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "replicas": { + "properties": { + "desired": { + "type": "long" + }, + "observed": { + "type": "long" + } + } + } + } + }, + "system": { + "properties": { + "_module": { + "properties": { + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "container": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu": { + "properties": { + "usage": { + "properties": { + "core": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "nanocores": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "majorpagefaults": { + "type": "long" + }, + "pagefaults": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "workingset": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "start_time": { + "type": "date" + } + } + }, + "volume": { + "properties": { + "_module": { + "properties": { + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "fs": { + "properties": { + "available": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "capacity": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inodes": { + "properties": { + "count": { + "type": "long" + }, + "free": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "kvm": { + "properties": { + "dommemstat": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "stat": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "type": "long" + } + } + } + } + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "logstash": { + "properties": { + "node": { + "properties": { + "jvm": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "stats": { + "properties": { + "events": { + "properties": { + "filtered": { + "type": "long" + }, + "in": { + "type": "long" + }, + "out": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "memcached": { + "properties": { + "stats": { + "properties": { + "bytes": { + "properties": { + "current": { + "type": "long" + }, + "limit": { + "type": "long" + } + } + }, + "cmd": { + "properties": { + "get": { + "type": "long" + }, + "set": { + "type": "long" + } + } + }, + "connections": { + "properties": { + "current": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "evictions": { + "type": "long" + }, + "get": { + "properties": { + "hits": { + "type": "long" + }, + "misses": { + "type": "long" + } + } + }, + "items": { + "properties": { + "current": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "pid": { + "type": "long" + }, + "read": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "threads": { + "type": "long" + }, + "uptime": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "written": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "metricset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mongodb": { + "properties": { + "collstats": { + "properties": { + "collection": { + "ignore_above": 1024, + "type": "keyword" + }, + "commands": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "db": { + "ignore_above": 1024, + "type": "keyword" + }, + "getmore": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "insert": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "lock": { + "properties": { + "read": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "write": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "queries": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "remove": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "update": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "dbstats": { + "properties": { + "avg_obj_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "collections": { + "type": "long" + }, + "data_file_version": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + } + } + }, + "data_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "db": { + "ignore_above": 1024, + "type": "keyword" + }, + "extent_free_list": { + "properties": { + "num": { + "type": "long" + }, + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "file_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "index_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "indexes": { + "type": "long" + }, + "ns_size_mb": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "num_extents": { + "type": "long" + }, + "objects": { + "type": "long" + }, + "storage_size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "metrics": { + "properties": { + "commands": { + "properties": { + "aggregate": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "build_info": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "coll_stats": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "connection_pool_stats": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "count": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "db_stats": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "distinct": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "find": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "get_cmd_line_opts": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "get_last_error": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "get_log": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "get_more": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "get_parameter": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "host_info": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "insert": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "is_master": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "is_self": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "last_collections": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "last_commands": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "list_databased": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "list_indexes": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "ping": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "profile": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "replset_get_rbid": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "replset_get_status": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "replset_heartbeat": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "replset_update_position": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "server_status": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "update": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "whatsmyuri": { + "properties": { + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + }, + "cursor": { + "properties": { + "open": { + "properties": { + "no_timeout": { + "type": "long" + }, + "pinned": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "timed_out": { + "type": "long" + } + } + }, + "document": { + "properties": { + "deleted": { + "type": "long" + }, + "inserted": { + "type": "long" + }, + "returned": { + "type": "long" + }, + "updated": { + "type": "long" + } + } + }, + "get_last_error": { + "properties": { + "write_timeouts": { + "type": "long" + }, + "write_wait": { + "properties": { + "count": { + "type": "long" + }, + "ms": { + "type": "long" + } + } + } + } + }, + "operation": { + "properties": { + "scan_and_order": { + "type": "long" + }, + "write_conflicts": { + "type": "long" + } + } + }, + "query_executor": { + "properties": { + "scanned_documents": { + "type": "long" + }, + "scanned_indexes": { + "type": "long" + } + } + }, + "replication": { + "properties": { + "apply": { + "properties": { + "attempts_to_become_secondary": { + "type": "long" + }, + "batches": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "ops": { + "type": "long" + } + } + }, + "buffer": { + "properties": { + "count": { + "type": "long" + }, + "max_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "executor": { + "properties": { + "counters": { + "properties": { + "cancels": { + "type": "long" + }, + "event_created": { + "type": "long" + }, + "event_wait": { + "type": "long" + }, + "scheduled": { + "properties": { + "dbwork": { + "type": "long" + }, + "exclusive": { + "type": "long" + }, + "failures": { + "type": "long" + }, + "netcmd": { + "type": "long" + }, + "work": { + "type": "long" + }, + "work_at": { + "type": "long" + } + } + }, + "waits": { + "type": "long" + } + } + }, + "event_waiters": { + "type": "long" + }, + "network_interface": { + "ignore_above": 1024, + "type": "keyword" + }, + "queues": { + "properties": { + "free": { + "type": "long" + }, + "in_progress": { + "properties": { + "dbwork": { + "type": "long" + }, + "exclusive": { + "type": "long" + }, + "network": { + "type": "long" + } + } + }, + "ready": { + "type": "long" + }, + "sleepers": { + "type": "long" + } + } + }, + "shutting_down": { + "type": "boolean" + }, + "unsignaled_events": { + "type": "long" + } + } + }, + "initial_sync": { + "properties": { + "completed": { + "type": "long" + }, + "failed_attempts": { + "type": "long" + }, + "failures": { + "type": "long" + } + } + }, + "network": { + "properties": { + "bytes": { + "type": "long" + }, + "getmores": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "ops": { + "type": "long" + }, + "reders_created": { + "type": "long" + } + } + }, + "preload": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indexes": { + "properties": { + "count": { + "type": "long" + }, + "time": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "storage": { + "properties": { + "free_list": { + "properties": { + "search": { + "properties": { + "bucket_exhausted": { + "type": "long" + }, + "requests": { + "type": "long" + }, + "scanned": { + "type": "long" + } + } + } + } + } + } + }, + "ttl": { + "properties": { + "deleted_documents": { + "type": "long" + }, + "passes": { + "type": "long" + } + } + } + } + }, + "replstatus": { + "properties": { + "headroom": { + "properties": { + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "lag": { + "properties": { + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "members": { + "properties": { + "arbiter": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "down": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "primary": { + "properties": { + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "optime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "recovering": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rollback": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secondary": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + }, + "optimes": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "startup2": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "unhealthy": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "unknown": { + "properties": { + "count": { + "type": "long" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "oplog": { + "properties": { + "first": { + "properties": { + "timestamp": { + "type": "long" + } + } + }, + "last": { + "properties": { + "timestamp": { + "type": "long" + } + } + }, + "size": { + "properties": { + "allocated": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "window": { + "type": "long" + } + } + }, + "optimes": { + "properties": { + "applied": { + "type": "long" + }, + "durable": { + "type": "long" + }, + "last_committed": { + "type": "long" + } + } + }, + "server_date": { + "type": "date" + }, + "set_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "status": { + "properties": { + "asserts": { + "properties": { + "msg": { + "type": "long" + }, + "regular": { + "type": "long" + }, + "rollovers": { + "type": "long" + }, + "user": { + "type": "long" + }, + "warning": { + "type": "long" + } + } + }, + "background_flushing": { + "properties": { + "average": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "flushes": { + "type": "long" + }, + "last": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "last_finished": { + "type": "date" + }, + "total": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "connections": { + "properties": { + "available": { + "type": "long" + }, + "current": { + "type": "long" + }, + "total_created": { + "type": "long" + } + } + }, + "extra_info": { + "properties": { + "heap_usage": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "page_faults": { + "type": "long" + } + } + }, + "global_lock": { + "properties": { + "active_clients": { + "properties": { + "readers": { + "type": "long" + }, + "total": { + "type": "long" + }, + "writers": { + "type": "long" + } + } + }, + "current_queue": { + "properties": { + "readers": { + "type": "long" + }, + "total": { + "type": "long" + }, + "writers": { + "type": "long" + } + } + }, + "total_time": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "journaling": { + "properties": { + "commits": { + "type": "long" + }, + "commits_in_write_lock": { + "type": "long" + }, + "compression": { + "type": "long" + }, + "early_commits": { + "type": "long" + }, + "journaled": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "times": { + "properties": { + "commits": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "commits_in_write_lock": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "dt": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "prep_log_buffer": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "remap_private_view": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "write_to_data_files": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "write_to_journal": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "write_to_data_files": { + "properties": { + "mb": { + "type": "long" + } + } + } + } + }, + "local_time": { + "type": "date" + }, + "locks": { + "properties": { + "collection": { + "properties": { + "acquire": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "deadlock": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "wait": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + }, + "us": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + } + } + }, + "database": { + "properties": { + "acquire": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "deadlock": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "wait": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + }, + "us": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + } + } + }, + "global": { + "properties": { + "acquire": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "deadlock": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "wait": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + }, + "us": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + } + } + }, + "meta_data": { + "properties": { + "acquire": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "deadlock": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "wait": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + }, + "us": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + } + } + }, + "oplog": { + "properties": { + "acquire": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "deadlock": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + }, + "wait": { + "properties": { + "count": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + }, + "us": { + "properties": { + "R": { + "type": "long" + }, + "W": { + "type": "long" + }, + "r": { + "type": "long" + }, + "w": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "memory": { + "properties": { + "bits": { + "type": "long" + }, + "mapped": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "mapped_with_journal": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "resident": { + "properties": { + "mb": { + "type": "long" + } + } + }, + "virtual": { + "properties": { + "mb": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "in": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "out": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "requests": { + "type": "long" + } + } + }, + "ops": { + "properties": { + "counters": { + "properties": { + "command": { + "type": "long" + }, + "delete": { + "type": "long" + }, + "getmore": { + "type": "long" + }, + "insert": { + "type": "long" + }, + "query": { + "type": "long" + }, + "update": { + "type": "long" + } + } + }, + "latencies": { + "properties": { + "commands": { + "properties": { + "count": { + "type": "long" + }, + "latency": { + "type": "long" + } + } + }, + "reads": { + "properties": { + "count": { + "type": "long" + }, + "latency": { + "type": "long" + } + } + }, + "writes": { + "properties": { + "count": { + "type": "long" + }, + "latency": { + "type": "long" + } + } + } + } + }, + "replicated": { + "properties": { + "command": { + "type": "long" + }, + "delete": { + "type": "long" + }, + "getmore": { + "type": "long" + }, + "insert": { + "type": "long" + }, + "query": { + "type": "long" + }, + "update": { + "type": "long" + } + } + } + } + }, + "process": { + "path": "process.name", + "type": "alias" + }, + "storage_engine": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "version": { + "path": "service.version", + "type": "alias" + }, + "wired_tiger": { + "properties": { + "cache": { + "properties": { + "dirty": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "maximum": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "pages": { + "properties": { + "evicted": { + "type": "long" + }, + "read": { + "type": "long" + }, + "write": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "concurrent_transactions": { + "properties": { + "read": { + "properties": { + "available": { + "type": "long" + }, + "out": { + "type": "long" + }, + "total_tickets": { + "type": "long" + } + } + }, + "write": { + "properties": { + "available": { + "type": "long" + }, + "out": { + "type": "long" + }, + "total_tickets": { + "type": "long" + } + } + } + } + }, + "log": { + "properties": { + "flushes": { + "type": "long" + }, + "max_file_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "scans": { + "type": "long" + }, + "size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "syncs": { + "type": "long" + }, + "write": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "writes": { + "type": "long" + } + } + } + } + }, + "write_backs_queued": { + "type": "boolean" + } + } + } + } + }, + "mssql": { + "properties": { + "database": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "performance": { + "properties": { + "active_temp_tables": { + "type": "long" + }, + "batch_requests_per_sec": { + "type": "long" + }, + "buffer": { + "properties": { + "cache_hit": { + "properties": { + "pct": { + "type": "double" + } + } + }, + "checkpoint_pages_per_sec": { + "type": "long" + }, + "database_pages": { + "type": "long" + }, + "page_life_expectancy": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "target_pages": { + "type": "long" + } + } + }, + "compilations_per_sec": { + "type": "long" + }, + "connections_reset_per_sec": { + "type": "long" + }, + "lock_waits_per_sec": { + "type": "long" + }, + "logins_per_sec": { + "type": "long" + }, + "logouts_per_sec": { + "type": "long" + }, + "page_splits_per_sec": { + "type": "long" + }, + "recompilations_per_sec": { + "type": "long" + }, + "transactions": { + "type": "long" + }, + "user_connections": { + "type": "long" + } + } + }, + "transaction_log": { + "properties": { + "space_usage": { + "properties": { + "since_last_backup": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "float" + } + } + } + } + }, + "stats": { + "properties": { + "active_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "backup_time": { + "type": "date" + }, + "recovery_size": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "since_last_checkpoint": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total_size": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "munin": { + "properties": { + "metrics": { + "properties": { + "*": { + "type": "object" + } + } + }, + "plugin": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "mysql": { + "properties": { + "galera_status": { + "properties": { + "apply": { + "properties": { + "oooe": { + "type": "double" + }, + "oool": { + "type": "double" + }, + "window": { + "type": "double" + } + } + }, + "cert": { + "properties": { + "deps_distance": { + "type": "double" + }, + "index_size": { + "type": "long" + }, + "interval": { + "type": "double" + } + } + }, + "cluster": { + "properties": { + "conf_id": { + "type": "long" + }, + "size": { + "type": "long" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "commit": { + "properties": { + "oooe": { + "type": "double" + }, + "window": { + "type": "long" + } + } + }, + "connected": { + "ignore_above": 1024, + "type": "keyword" + }, + "evs": { + "properties": { + "evict": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "flow_ctl": { + "properties": { + "paused": { + "type": "double" + }, + "paused_ns": { + "type": "long" + }, + "recv": { + "type": "long" + }, + "sent": { + "type": "long" + } + } + }, + "last_committed": { + "type": "long" + }, + "local": { + "properties": { + "bf_aborts": { + "type": "long" + }, + "cert_failures": { + "type": "long" + }, + "commits": { + "type": "long" + }, + "recv": { + "properties": { + "queue": { + "type": "long" + }, + "queue_avg": { + "type": "double" + }, + "queue_max": { + "type": "long" + }, + "queue_min": { + "type": "long" + } + } + }, + "replays": { + "type": "long" + }, + "send": { + "properties": { + "queue": { + "type": "long" + }, + "queue_avg": { + "type": "double" + }, + "queue_max": { + "type": "long" + }, + "queue_min": { + "type": "long" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ready": { + "ignore_above": 1024, + "type": "keyword" + }, + "received": { + "properties": { + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + } + } + }, + "repl": { + "properties": { + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + }, + "data_bytes": { + "type": "long" + }, + "keys": { + "type": "long" + }, + "keys_bytes": { + "type": "long" + }, + "other_bytes": { + "type": "long" + } + } + } + } + }, + "status": { + "properties": { + "aborted": { + "properties": { + "clients": { + "type": "long" + }, + "connects": { + "type": "long" + } + } + }, + "binlog": { + "properties": { + "cache": { + "properties": { + "disk_use": { + "type": "long" + }, + "use": { + "type": "long" + } + } + } + } + }, + "bytes": { + "properties": { + "received": { + "type": "long" + }, + "sent": { + "type": "long" + } + } + }, + "command": { + "properties": { + "delete": { + "type": "long" + }, + "insert": { + "type": "long" + }, + "select": { + "type": "long" + }, + "update": { + "type": "long" + } + } + }, + "connections": { + "type": "long" + }, + "created": { + "properties": { + "tmp": { + "properties": { + "disk_tables": { + "type": "long" + }, + "files": { + "type": "long" + }, + "tables": { + "type": "long" + } + } + } + } + }, + "delayed": { + "properties": { + "errors": { + "type": "long" + }, + "insert_threads": { + "type": "long" + }, + "writes": { + "type": "long" + } + } + }, + "flush_commands": { + "type": "long" + }, + "handler": { + "properties": { + "commit": { + "type": "long" + }, + "delete": { + "type": "long" + }, + "external_lock": { + "type": "long" + }, + "mrr_init": { + "type": "long" + }, + "prepare": { + "type": "long" + }, + "read": { + "properties": { + "first": { + "type": "long" + }, + "key": { + "type": "long" + }, + "last": { + "type": "long" + }, + "next": { + "type": "long" + }, + "prev": { + "type": "long" + }, + "rnd": { + "type": "long" + }, + "rnd_next": { + "type": "long" + } + } + }, + "rollback": { + "type": "long" + }, + "savepoint": { + "type": "long" + }, + "savepoint_rollback": { + "type": "long" + }, + "update": { + "type": "long" + }, + "write": { + "type": "long" + } + } + }, + "innodb": { + "properties": { + "buffer_pool": { + "properties": { + "bytes": { + "properties": { + "data": { + "type": "long" + }, + "dirty": { + "type": "long" + } + } + }, + "dump_status": { + "type": "long" + }, + "load_status": { + "type": "long" + }, + "pages": { + "properties": { + "data": { + "type": "long" + }, + "dirty": { + "type": "long" + }, + "flushed": { + "type": "long" + }, + "free": { + "type": "long" + }, + "latched": { + "type": "long" + }, + "misc": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "pool": { + "properties": { + "reads": { + "type": "long" + }, + "resize_status": { + "type": "long" + }, + "wait_free": { + "type": "long" + } + } + }, + "read": { + "properties": { + "ahead": { + "type": "long" + }, + "ahead_evicted": { + "type": "long" + }, + "ahead_rnd": { + "type": "long" + }, + "requests": { + "type": "long" + } + } + }, + "write_requests": { + "type": "long" + } + } + } + } + }, + "max_used_connections": { + "type": "long" + }, + "open": { + "properties": { + "files": { + "type": "long" + }, + "streams": { + "type": "long" + }, + "tables": { + "type": "long" + } + } + }, + "opened_tables": { + "type": "long" + }, + "queries": { + "type": "long" + }, + "questions": { + "type": "long" + }, + "threads": { + "properties": { + "cached": { + "type": "long" + }, + "connected": { + "type": "long" + }, + "created": { + "type": "long" + }, + "running": { + "type": "long" + } + } + } + } + } + } + }, + "nats": { + "properties": { + "connections": { + "properties": { + "total": { + "type": "long" + } + } + }, + "routes": { + "properties": { + "total": { + "type": "long" + } + } + }, + "server": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "time": { + "type": "date" + } + } + }, + "stats": { + "properties": { + "cores": { + "type": "long" + }, + "cpu": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "http": { + "properties": { + "req_stats": { + "properties": { + "uri": { + "properties": { + "connz": { + "type": "long" + }, + "root": { + "type": "long" + }, + "routez": { + "type": "long" + }, + "subsz": { + "type": "long" + }, + "varz": { + "type": "long" + } + } + } + } + } + } + }, + "in": { + "properties": { + "bytes": { + "type": "long" + }, + "messages": { + "type": "long" + } + } + }, + "mem": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "out": { + "properties": { + "bytes": { + "type": "long" + }, + "messages": { + "type": "long" + } + } + }, + "remotes": { + "type": "long" + }, + "slow_consumers": { + "type": "long" + }, + "total_connections": { + "type": "long" + }, + "uptime": { + "type": "long" + } + } + }, + "subscriptions": { + "properties": { + "cache": { + "properties": { + "fanout": { + "properties": { + "avg": { + "type": "double" + }, + "max": { + "type": "long" + } + } + }, + "hit_rate": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "size": { + "type": "long" + } + } + }, + "inserts": { + "type": "long" + }, + "matches": { + "type": "long" + }, + "removes": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "nginx": { + "properties": { + "stubstatus": { + "properties": { + "accepts": { + "type": "long" + }, + "active": { + "type": "long" + }, + "current": { + "type": "long" + }, + "dropped": { + "type": "long" + }, + "handled": { + "type": "long" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "reading": { + "type": "long" + }, + "requests": { + "type": "long" + }, + "waiting": { + "type": "long" + }, + "writing": { + "type": "long" + } + } + } + } + }, + "observer": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "oracle": { + "properties": { + "tablespace": { + "properties": { + "data_file": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "online_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "properties": { + "bytes": { + "type": "long" + }, + "free": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "space": { + "properties": { + "free": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "php_fpm": { + "properties": { + "pool": { + "properties": { + "connections": { + "properties": { + "accepted": { + "type": "long" + }, + "listen_queue_len": { + "type": "long" + }, + "max_listen_queue": { + "type": "long" + }, + "queued": { + "type": "long" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "process_manager": { + "ignore_above": 1024, + "type": "keyword" + }, + "processes": { + "properties": { + "active": { + "type": "long" + }, + "idle": { + "type": "long" + }, + "max_active": { + "type": "long" + }, + "max_children_reached": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "slow_requests": { + "type": "long" + }, + "start_since": { + "type": "long" + }, + "start_time": { + "type": "date" + } + } + }, + "process": { + "properties": { + "last_request_cpu": { + "type": "long" + }, + "last_request_memory": { + "type": "long" + }, + "request_duration": { + "type": "long" + }, + "requests": { + "type": "long" + }, + "script": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_since": { + "type": "long" + }, + "start_time": { + "type": "date" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "postgresql": { + "properties": { + "activity": { + "properties": { + "application_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "backend_start": { + "type": "date" + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + } + } + }, + "database": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "oid": { + "type": "long" + } + } + }, + "pid": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "query_start": { + "type": "date" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_change": { + "type": "date" + }, + "transaction_start": { + "type": "date" + }, + "user": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "waiting": { + "type": "boolean" + } + } + }, + "bgwriter": { + "properties": { + "buffers": { + "properties": { + "allocated": { + "type": "long" + }, + "backend": { + "type": "long" + }, + "backend_fsync": { + "type": "long" + }, + "checkpoints": { + "type": "long" + }, + "clean": { + "type": "long" + }, + "clean_full": { + "type": "long" + } + } + }, + "checkpoints": { + "properties": { + "requested": { + "type": "long" + }, + "scheduled": { + "type": "long" + }, + "times": { + "properties": { + "sync": { + "properties": { + "ms": { + "type": "float" + } + } + }, + "write": { + "properties": { + "ms": { + "type": "float" + } + } + } + } + } + } + }, + "stats_reset": { + "type": "date" + } + } + }, + "database": { + "properties": { + "blocks": { + "properties": { + "hit": { + "type": "long" + }, + "read": { + "type": "long" + }, + "time": { + "properties": { + "read": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "write": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "conflicts": { + "type": "long" + }, + "deadlocks": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "number_of_backends": { + "type": "long" + }, + "oid": { + "type": "long" + }, + "rows": { + "properties": { + "deleted": { + "type": "long" + }, + "fetched": { + "type": "long" + }, + "inserted": { + "type": "long" + }, + "returned": { + "type": "long" + }, + "updated": { + "type": "long" + } + } + }, + "stats_reset": { + "type": "date" + }, + "temporary": { + "properties": { + "bytes": { + "type": "long" + }, + "files": { + "type": "long" + } + } + }, + "transactions": { + "properties": { + "commit": { + "type": "long" + }, + "rollback": { + "type": "long" + } + } + } + } + }, + "statement": { + "properties": { + "database": { + "properties": { + "oid": { + "type": "long" + } + } + }, + "query": { + "properties": { + "calls": { + "type": "long" + }, + "id": { + "type": "long" + }, + "memory": { + "properties": { + "local": { + "properties": { + "dirtied": { + "type": "long" + }, + "hit": { + "type": "long" + }, + "read": { + "type": "long" + }, + "written": { + "type": "long" + } + } + }, + "shared": { + "properties": { + "dirtied": { + "type": "long" + }, + "hit": { + "type": "long" + }, + "read": { + "type": "long" + }, + "written": { + "type": "long" + } + } + }, + "temp": { + "properties": { + "read": { + "type": "long" + }, + "written": { + "type": "long" + } + } + } + } + }, + "rows": { + "type": "long" + }, + "text": { + "ignore_above": 1024, + "type": "keyword" + }, + "time": { + "properties": { + "max": { + "properties": { + "ms": { + "type": "float" + } + } + }, + "mean": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "min": { + "properties": { + "ms": { + "type": "float" + } + } + }, + "stddev": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "total": { + "properties": { + "ms": { + "type": "float" + } + } + } + } + } + } + }, + "user": { + "properties": { + "id": { + "type": "long" + } + } + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "prometheus": { + "properties": { + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "metrics": { + "properties": { + "*": { + "type": "object" + } + } + } + } + }, + "rabbitmq": { + "properties": { + "connection": { + "properties": { + "channel_max": { + "type": "long" + }, + "channels": { + "type": "long" + }, + "client_provided": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "frame_max": { + "type": "long" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "octet_count": { + "properties": { + "received": { + "type": "long" + }, + "sent": { + "type": "long" + } + } + }, + "packet_count": { + "properties": { + "pending": { + "type": "long" + }, + "received": { + "type": "long" + }, + "sent": { + "type": "long" + } + } + }, + "peer": { + "properties": { + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + } + } + }, + "port": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "exchange": { + "properties": { + "auto_delete": { + "type": "boolean" + }, + "durable": { + "type": "boolean" + }, + "internal": { + "type": "boolean" + }, + "messages": { + "properties": { + "publish_in": { + "properties": { + "count": { + "type": "long" + }, + "details": { + "properties": { + "rate": { + "type": "float" + } + } + } + } + }, + "publish_out": { + "properties": { + "count": { + "type": "long" + }, + "details": { + "properties": { + "rate": { + "type": "float" + } + } + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "node": { + "properties": { + "disk": { + "properties": { + "free": { + "properties": { + "bytes": { + "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "fd": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "gc": { + "properties": { + "num": { + "properties": { + "count": { + "type": "long" + } + } + }, + "reclaimed": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "io": { + "properties": { + "file_handle": { + "properties": { + "open_attempt": { + "properties": { + "avg": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "count": { + "type": "long" + } + } + } + } + }, + "read": { + "properties": { + "avg": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + } + } + }, + "reopen": { + "properties": { + "count": { + "type": "long" + } + } + }, + "seek": { + "properties": { + "avg": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "count": { + "type": "long" + } + } + }, + "sync": { + "properties": { + "avg": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "count": { + "type": "long" + } + } + }, + "write": { + "properties": { + "avg": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + } + } + } + } + }, + "mem": { + "properties": { + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "mnesia": { + "properties": { + "disk": { + "properties": { + "tx": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "ram": { + "properties": { + "tx": { + "properties": { + "count": { + "type": "long" + } + } + } + } + } + } + }, + "msg": { + "properties": { + "store_read": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store_write": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "proc": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "processors": { + "type": "long" + }, + "queue": { + "properties": { + "index": { + "properties": { + "journal_write": { + "properties": { + "count": { + "type": "long" + } + } + }, + "read": { + "properties": { + "count": { + "type": "long" + } + } + }, + "write": { + "properties": { + "count": { + "type": "long" + } + } + } + } + } + } + }, + "run": { + "properties": { + "queue": { + "type": "long" + } + } + }, + "socket": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "type": "long" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "arguments": { + "properties": { + "max_priority": { + "type": "long" + } + } + }, + "auto_delete": { + "type": "boolean" + }, + "consumers": { + "properties": { + "count": { + "type": "long" + }, + "utilisation": { + "properties": { + "pct": { + "type": "long" + } + } + } + } + }, + "disk": { + "properties": { + "reads": { + "properties": { + "count": { + "type": "long" + } + } + }, + "writes": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "durable": { + "type": "boolean" + }, + "exclusive": { + "type": "boolean" + }, + "memory": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "messages": { + "properties": { + "persistent": { + "properties": { + "count": { + "type": "long" + } + } + }, + "ready": { + "properties": { + "count": { + "type": "long" + }, + "details": { + "properties": { + "rate": { + "type": "float" + } + } + } + } + }, + "total": { + "properties": { + "count": { + "type": "long" + }, + "details": { + "properties": { + "rate": { + "type": "float" + } + } + } + } + }, + "unacknowledged": { + "properties": { + "count": { + "type": "long" + }, + "details": { + "properties": { + "rate": { + "type": "float" + } + } + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vhost": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "redis": { + "properties": { + "info": { + "properties": { + "clients": { + "properties": { + "biggest_input_buf": { + "type": "long" + }, + "blocked": { + "type": "long" + }, + "connected": { + "type": "long" + }, + "longest_output_list": { + "type": "long" + }, + "max_input_buffer": { + "type": "long" + }, + "max_output_buffer": { + "type": "long" + } + } + }, + "cluster": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "cpu": { + "properties": { + "used": { + "properties": { + "sys": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "sys_children": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "user": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "user_children": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "memory": { + "properties": { + "active_defrag": { + "properties": { + "is_running": { + "type": "boolean" + } + } + }, + "allocator": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocator_stats": { + "properties": { + "active": { + "type": "long" + }, + "allocated": { + "type": "long" + }, + "fragmentation": { + "properties": { + "bytes": { + "type": "long" + }, + "ratio": { + "type": "float" + } + } + }, + "resident": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + }, + "ratio": { + "type": "float" + } + } + } + } + }, + "fragmentation": { + "properties": { + "bytes": { + "type": "long" + }, + "ratio": { + "type": "float" + } + } + }, + "max": { + "properties": { + "policy": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "type": "long" + } + } + }, + "used": { + "properties": { + "dataset": { + "type": "long" + }, + "lua": { + "type": "long" + }, + "peak": { + "type": "long" + }, + "rss": { + "type": "long" + }, + "value": { + "type": "long" + } + } + } + } + }, + "persistence": { + "properties": { + "aof": { + "properties": { + "bgrewrite": { + "properties": { + "last_status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "buffer": { + "properties": { + "size": { + "type": "long" + } + } + }, + "copy_on_write": { + "properties": { + "last_size": { + "type": "long" + } + } + }, + "enabled": { + "type": "boolean" + }, + "fsync": { + "properties": { + "delayed": { + "type": "long" + }, + "pending": { + "type": "long" + } + } + }, + "rewrite": { + "properties": { + "buffer": { + "properties": { + "size": { + "type": "long" + } + } + }, + "current_time": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "in_progress": { + "type": "boolean" + }, + "last_time": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "scheduled": { + "type": "boolean" + } + } + }, + "size": { + "properties": { + "base": { + "type": "long" + }, + "current": { + "type": "long" + } + } + }, + "write": { + "properties": { + "last_status": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "loading": { + "type": "boolean" + }, + "rdb": { + "properties": { + "bgsave": { + "properties": { + "current_time": { + "properties": { + "sec": { + "type": "long" + } + } + }, + "in_progress": { + "type": "boolean" + }, + "last_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "last_time": { + "properties": { + "sec": { + "type": "long" + } + } + } + } + }, + "copy_on_write": { + "properties": { + "last_size": { + "type": "long" + } + } + }, + "last_save": { + "properties": { + "changes_since": { + "type": "long" + }, + "time": { + "type": "long" + } + } + } + } + } + } + }, + "replication": { + "properties": { + "backlog": { + "properties": { + "active": { + "type": "long" + }, + "first_byte_offset": { + "type": "long" + }, + "histlen": { + "type": "long" + }, + "size": { + "type": "long" + } + } + }, + "connected_slaves": { + "type": "long" + }, + "master": { + "properties": { + "last_io_seconds_ago": { + "type": "long" + }, + "link_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "offset": { + "type": "long" + }, + "second_offset": { + "type": "long" + }, + "sync": { + "properties": { + "in_progress": { + "type": "boolean" + }, + "last_io_seconds_ago": { + "type": "long" + }, + "left_bytes": { + "type": "long" + } + } + } + } + }, + "master_offset": { + "type": "long" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "slave": { + "properties": { + "is_readonly": { + "type": "boolean" + }, + "offset": { + "type": "long" + }, + "priority": { + "type": "long" + } + } + } + } + }, + "server": { + "properties": { + "arch_bits": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "config_file": { + "ignore_above": 1024, + "type": "keyword" + }, + "gcc_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "git_dirty": { + "ignore_above": 1024, + "type": "keyword" + }, + "git_sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "hz": { + "type": "long" + }, + "lru_clock": { + "type": "long" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "multiplexing_api": { + "ignore_above": 1024, + "type": "keyword" + }, + "run_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcp_port": { + "type": "long" + }, + "uptime": { + "type": "long" + } + } + }, + "slowlog": { + "properties": { + "count": { + "type": "long" + } + } + }, + "stats": { + "properties": { + "active_defrag": { + "properties": { + "hits": { + "type": "long" + }, + "key_hits": { + "type": "long" + }, + "key_misses": { + "type": "long" + }, + "misses": { + "type": "long" + } + } + }, + "commands_processed": { + "type": "long" + }, + "connections": { + "properties": { + "received": { + "type": "long" + }, + "rejected": { + "type": "long" + } + } + }, + "instantaneous": { + "properties": { + "input_kbps": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ops_per_sec": { + "type": "long" + }, + "output_kbps": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "keys": { + "properties": { + "evicted": { + "type": "long" + }, + "expired": { + "type": "long" + } + } + }, + "keyspace": { + "properties": { + "hits": { + "type": "long" + }, + "misses": { + "type": "long" + } + } + }, + "latest_fork_usec": { + "type": "long" + }, + "migrate_cached_sockets": { + "type": "long" + }, + "net": { + "properties": { + "input": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "output": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "pubsub": { + "properties": { + "channels": { + "type": "long" + }, + "patterns": { + "type": "long" + } + } + }, + "slave_expires_tracked_keys": { + "type": "long" + }, + "sync": { + "properties": { + "full": { + "type": "long" + }, + "partial": { + "properties": { + "err": { + "type": "long" + }, + "ok": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "key": { + "properties": { + "expire": { + "properties": { + "ttl": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "length": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "keyspace": { + "properties": { + "avg_ttl": { + "type": "long" + }, + "expires": { + "type": "long" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "keys": { + "type": "long" + } + } + } + } + }, + "related": { + "properties": { + "ip": { + "type": "ip" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "system": { + "properties": { + "core": { + "properties": { + "id": { + "type": "long" + }, + "idle": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "iowait": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "irq": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "nice": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "softirq": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "steal": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "system": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "user": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + } + } + }, + "cpu": { + "properties": { + "cores": { + "type": "long" + }, + "idle": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "iowait": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "irq": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "nice": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "softirq": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "steal": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "system": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + }, + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "user": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + } + } + } + } + }, + "diskio": { + "properties": { + "io": { + "properties": { + "time": { + "type": "long" + } + } + }, + "iostat": { + "properties": { + "await": { + "type": "float" + }, + "busy": { + "type": "float" + }, + "queue": { + "properties": { + "avg_size": { + "type": "float" + } + } + }, + "read": { + "properties": { + "await": { + "type": "float" + }, + "per_sec": { + "properties": { + "bytes": { + "type": "float" + } + } + }, + "request": { + "properties": { + "merges_per_sec": { + "type": "float" + }, + "per_sec": { + "type": "float" + } + } + } + } + }, + "request": { + "properties": { + "avg_size": { + "type": "float" + } + } + }, + "service_time": { + "type": "float" + }, + "write": { + "properties": { + "await": { + "type": "float" + }, + "per_sec": { + "properties": { + "bytes": { + "type": "float" + } + } + }, + "request": { + "properties": { + "merges_per_sec": { + "type": "float" + }, + "per_sec": { + "type": "float" + } + } + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "read": { + "properties": { + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + }, + "time": { + "type": "long" + } + } + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "write": { + "properties": { + "bytes": { + "type": "long" + }, + "count": { + "type": "long" + }, + "time": { + "type": "long" + } + } + } + } + }, + "entropy": { + "properties": { + "available_bits": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "filesystem": { + "properties": { + "available": { + "type": "long" + }, + "device_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "files": { + "type": "long" + }, + "free": { + "type": "long" + }, + "free_files": { + "type": "long" + }, + "mount_point": { + "ignore_above": 1024, + "type": "keyword" + }, + "total": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "fsstat": { + "properties": { + "count": { + "type": "long" + }, + "total_files": { + "type": "long" + }, + "total_size": { + "properties": { + "free": { + "type": "long" + }, + "total": { + "type": "long" + }, + "used": { + "type": "long" + } + } + } + } + }, + "load": { + "properties": { + "1": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "15": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "5": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "cores": { + "type": "long" + }, + "norm": { + "properties": { + "1": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "15": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "5": { + "scaling_factor": 100, + "type": "scaled_float" + } + } + } + } + }, + "memory": { + "properties": { + "actual": { + "properties": { + "free": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "free": { + "type": "long" + }, + "hugepages": { + "properties": { + "default_size": { + "type": "long" + }, + "free": { + "type": "long" + }, + "reserved": { + "type": "long" + }, + "surplus": { + "type": "long" + }, + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "long" + } + } + } + } + }, + "swap": { + "properties": { + "free": { + "type": "long" + }, + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "network": { + "properties": { + "in": { + "properties": { + "bytes": { + "type": "long" + }, + "dropped": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "out": { + "properties": { + "bytes": { + "type": "long" + }, + "dropped": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + } + } + }, + "process": { + "properties": { + "cgroup": { + "properties": { + "blkio": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "total": { + "properties": { + "bytes": { + "type": "long" + }, + "ios": { + "type": "long" + } + } + } + } + }, + "cpu": { + "properties": { + "cfs": { + "properties": { + "period": { + "properties": { + "us": { + "type": "long" + } + } + }, + "quota": { + "properties": { + "us": { + "type": "long" + } + } + }, + "shares": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rt": { + "properties": { + "period": { + "properties": { + "us": { + "type": "long" + } + } + }, + "runtime": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "stats": { + "properties": { + "periods": { + "type": "long" + }, + "throttled": { + "properties": { + "ns": { + "type": "long" + }, + "periods": { + "type": "long" + } + } + } + } + } + } + }, + "cpuacct": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "percpu": { + "properties": { + "1": { + "type": "long" + }, + "2": { + "type": "long" + }, + "3": { + "type": "long" + }, + "4": { + "type": "long" + } + } + }, + "stats": { + "properties": { + "system": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "user": { + "properties": { + "ns": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "ns": { + "type": "long" + } + } + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "kmem": { + "properties": { + "failures": { + "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "kmem_tcp": { + "properties": { + "failures": { + "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "mem": { + "properties": { + "failures": { + "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "memsw": { + "properties": { + "failures": { + "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "stats": { + "properties": { + "active_anon": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "active_file": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "cache": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "hierarchical_memory_limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "hierarchical_memsw_limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inactive_anon": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "inactive_file": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "major_page_faults": { + "type": "long" + }, + "mapped_file": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "page_faults": { + "type": "long" + }, + "pages_in": { + "type": "long" + }, + "pages_out": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "rss_huge": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "swap": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "unevictable": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "cmdline": { + "ignore_above": 2048, + "type": "keyword" + }, + "cpu": { + "properties": { + "start_time": { + "type": "date" + }, + "system": { + "properties": { + "ticks": { + "type": "long" + } + } + }, + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + }, + "ticks": { + "type": "long" + }, + "value": { + "type": "long" + } + } + }, + "user": { + "properties": { + "ticks": { + "type": "long" + } + } + } + } + }, + "env": { + "type": "object" + }, + "fd": { + "properties": { + "limit": { + "properties": { + "hard": { + "type": "long" + }, + "soft": { + "type": "long" + } + } + }, + "open": { + "type": "long" + } + } + }, + "memory": { + "properties": { + "rss": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "share": { + "type": "long" + }, + "size": { + "type": "long" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "summary": { + "properties": { + "dead": { + "type": "long" + }, + "idle": { + "type": "long" + }, + "running": { + "type": "long" + }, + "sleeping": { + "type": "long" + }, + "stopped": { + "type": "long" + }, + "total": { + "type": "long" + }, + "unknown": { + "type": "long" + }, + "zombie": { + "type": "long" + } + } + } + } + }, + "raid": { + "properties": { + "blocks": { + "properties": { + "synced": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "disks": { + "properties": { + "active": { + "type": "long" + }, + "failed": { + "type": "long" + }, + "spare": { + "type": "long" + }, + "states": { + "properties": { + "*": { + "type": "object" + } + } + }, + "total": { + "type": "long" + } + } + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "sync_action": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socket": { + "properties": { + "local": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "process": { + "properties": { + "cmdline": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "remote": { + "properties": { + "etld_plus_one": { + "ignore_above": 1024, + "type": "keyword" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "host_error": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "summary": { + "properties": { + "all": { + "properties": { + "count": { + "type": "long" + }, + "listening": { + "type": "long" + } + } + }, + "tcp": { + "properties": { + "all": { + "properties": { + "close_wait": { + "type": "long" + }, + "count": { + "type": "long" + }, + "established": { + "type": "long" + }, + "listening": { + "type": "long" + }, + "orphan": { + "type": "long" + }, + "time_wait": { + "type": "long" + } + } + }, + "memory": { + "type": "long" + } + } + }, + "udp": { + "properties": { + "all": { + "properties": { + "count": { + "type": "long" + } + } + }, + "memory": { + "type": "long" + } + } + } + } + } + } + }, + "uptime": { + "properties": { + "duration": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "traefik": { + "properties": { + "health": { + "properties": { + "response": { + "properties": { + "avg_time": { + "properties": { + "us": { + "type": "long" + } + } + }, + "count": { + "type": "long" + }, + "status_codes": { + "properties": { + "*": { + "type": "object" + } + } + } + } + }, + "uptime": { + "properties": { + "sec": { + "type": "long" + } + } + } + } + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uwsgi": { + "properties": { + "status": { + "properties": { + "core": { + "properties": { + "id": { + "type": "long" + }, + "read_errors": { + "type": "long" + }, + "requests": { + "properties": { + "offloaded": { + "type": "long" + }, + "routed": { + "type": "long" + }, + "static": { + "type": "long" + }, + "total": { + "type": "long" + } + } + }, + "worker_pid": { + "type": "long" + }, + "write_errors": { + "type": "long" + } + } + }, + "total": { + "properties": { + "exceptions": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "read_errors": { + "type": "long" + }, + "requests": { + "type": "long" + }, + "write_errors": { + "type": "long" + } + } + }, + "worker": { + "properties": { + "accepting": { + "type": "long" + }, + "avg_rt": { + "type": "long" + }, + "delta_requests": { + "type": "long" + }, + "exceptions": { + "type": "long" + }, + "harakiri_count": { + "type": "long" + }, + "id": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "requests": { + "type": "long" + }, + "respawn_count": { + "type": "long" + }, + "rss": { + "ignore_above": 1024, + "type": "keyword" + }, + "running_time": { + "type": "long" + }, + "signal_queue": { + "type": "long" + }, + "signals": { + "type": "long" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "tx": { + "type": "long" + }, + "vsz": { + "type": "long" + } + } + } + } + } + } + }, + "vsphere": { + "properties": { + "datastore": { + "properties": { + "capacity": { + "properties": { + "free": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "long" + } + } + } + } + }, + "fstype": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "cpu": { + "properties": { + "free": { + "properties": { + "mhz": { + "type": "long" + } + } + }, + "total": { + "properties": { + "mhz": { + "type": "long" + } + } + }, + "used": { + "properties": { + "mhz": { + "type": "long" + } + } + } + } + }, + "memory": { + "properties": { + "free": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "total": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "network_names": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "virtualmachine": { + "properties": { + "cpu": { + "properties": { + "used": { + "properties": { + "mhz": { + "type": "long" + } + } + } + } + }, + "custom_fields": { + "type": "object" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "properties": { + "free": { + "properties": { + "guest": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "guest": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "used": { + "properties": { + "guest": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "host": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "network_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "windows": { + "properties": { + "service": { + "properties": { + "display_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "start_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "zookeeper": { + "properties": { + "connection": { + "properties": { + "interest_ops": { + "type": "long" + }, + "queued": { + "type": "long" + }, + "received": { + "type": "long" + }, + "sent": { + "type": "long" + } + } + }, + "mntr": { + "properties": { + "approximate_data_size": { + "type": "long" + }, + "ephemerals_count": { + "type": "long" + }, + "followers": { + "type": "long" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "latency": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "max_file_descriptor_count": { + "type": "long" + }, + "num_alive_connections": { + "type": "long" + }, + "open_file_descriptor_count": { + "type": "long" + }, + "outstanding_requests": { + "type": "long" + }, + "packets": { + "properties": { + "received": { + "type": "long" + }, + "sent": { + "type": "long" + } + } + }, + "pending_syncs": { + "type": "long" + }, + "server_state": { + "ignore_above": 1024, + "type": "keyword" + }, + "synced_followers": { + "type": "long" + }, + "version": { + "path": "service.version", + "type": "alias" + }, + "watch_count": { + "type": "long" + }, + "znode_count": { + "type": "long" + } + } + }, + "server": { + "properties": { + "connections": { + "type": "long" + }, + "count": { + "type": "long" + }, + "epoch": { + "type": "long" + }, + "latency": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "node_count": { + "type": "long" + }, + "outstanding": { + "type": "long" + }, + "received": { + "type": "long" + }, + "sent": { + "type": "long" + }, + "version_date": { + "type": "date" + }, + "zxid": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "query": { + "default_field": [ + "beat.*", + "type", + "tags", + "meta.*", + "message" + ] + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/reporting/configs/generate_api.js b/x-pack/test/reporting/configs/generate_api.js index cd81dea583fff4..92f16d0951ee51 100644 --- a/x-pack/test/reporting/configs/generate_api.js +++ b/x-pack/test/reporting/configs/generate_api.js @@ -4,10 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { esTestConfig, kbnTestConfig } from '@kbn/test'; +import { format as formatUrl } from 'url'; import { getApiIntegrationConfig } from '../../api_integration/config'; import { getReportingApiConfig } from './api'; export default async function ({ readConfigFile }) { + const servers = { + kibana: kbnTestConfig.getUrlParts(), + elasticsearch: esTestConfig.getUrlParts(), + }; + const apiTestConfig = await getApiIntegrationConfig({ readConfigFile }); const reportingApiConfig = await getReportingApiConfig({ readConfigFile }); const xPackFunctionalTestsConfig = await readConfigFile(require.resolve('../../functional/config.js')); @@ -23,8 +30,16 @@ export default async function ({ readConfigFile }) { kbnTestServer: { ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ - ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.reporting.csv.enablePanelActionDownload=true', + `--optimize.enabled=false`, + `--logging.json=false`, + `--server.maxPayloadBytes=1679958`, + `--server.port=${kbnTestConfig.getPort()}`, + `--elasticsearch.hosts=${formatUrl(servers.elasticsearch)}`, + `--elasticsearch.password=${servers.elasticsearch.password}`, + `--elasticsearch.username=${servers.elasticsearch.username}`, + `--xpack.reporting.csv.enablePanelActionDownload=true`, + `--xpack.reporting.csv.maxSizeBytes=2850`, + `--xpack.reporting.queue.pollInterval=3000`, ], }, esArchiver: apiTestConfig.esArchiver, diff --git a/x-pack/test_utils/enzyme_helpers.tsx b/x-pack/test_utils/enzyme_helpers.tsx index d905ce433041b2..64bb3b07686fca 100644 --- a/x-pack/test_utils/enzyme_helpers.tsx +++ b/x-pack/test_utils/enzyme_helpers.tsx @@ -191,3 +191,5 @@ export const mountHook = ( hookValueCallback, }; }; + +export const nextTick = () => new Promise(res => process.nextTick(res)); diff --git a/yarn.lock b/yarn.lock index 4cb975f6b4669f..7213859589c269 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1611,10 +1611,10 @@ ts-debounce "^1.0.0" uuid "^3.3.2" -"@elastic/ctags-langserver@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@elastic/ctags-langserver/-/ctags-langserver-0.1.3.tgz#266b94e6fb0c8208c5af4b0de3898ecda4ab9344" - integrity sha512-/UzcY8a7iPo/m6GQZGhfgAEOy/CejmA6RzbKD6198A2ePUAuex2CQ8iS0de/1bezICaJ5hpbU9tRhMHHMaRY3A== +"@elastic/ctags-langserver@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@elastic/ctags-langserver/-/ctags-langserver-0.1.5.tgz#3d2691245ddb47c121178ebcdbf21a2adb74b3c2" + integrity sha512-Wx+1hv9u54ypEKGP1ZR/hp6z8nyjheGeOZRucO2LuMwaxNx8M8K/tDGQmAGRmXytW8f42QFAWo7PHFRht+fdSQ== dependencies: "@elastic/lsp-extension" "^0.1.1" "@elastic/node-ctags" "1.0.2" @@ -7900,6 +7900,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + check-disk-space@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-2.1.0.tgz#2e77fe62f30d9676dc37a524ea2008f40c780295" @@ -9248,6 +9253,11 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -9509,10 +9519,10 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -cypress@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.3.1.tgz#8a127b1d9fa74bff21f111705abfef58d595fdef" - integrity sha512-JIo47ZD9P3jAw7oaK7YKUoODzszJbNw41JmBrlMMiupHOlhmXvZz75htuo7mfRFPC9/1MDQktO4lX/V2+a6lGQ== +cypress@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.4.1.tgz#ca2e4e9864679da686c6a6189603efd409664c30" + integrity sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/xvfb" "1.2.4" @@ -9527,20 +9537,19 @@ cypress@^3.3.1: execa "0.10.0" executable "4.1.1" extract-zip "1.6.7" - fs-extra "4.0.1" + fs-extra "5.0.0" getos "3.1.1" - glob "7.1.3" is-ci "1.2.1" is-installed-globally "0.1.0" lazy-ass "1.6.0" listr "0.12.0" - lodash "4.17.11" + lodash "4.17.15" log-symbols "2.2.0" minimist "1.2.0" moment "2.24.0" ramda "0.24.1" request "2.88.0" - request-progress "0.4.0" + request-progress "3.0.0" supports-color "5.5.0" tmp "0.1.0" url "0.11.0" @@ -9862,6 +9871,11 @@ dateformat@^2.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI= +dateformat@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + debug-fabulous@1.X: version "1.1.0" resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-1.1.0.tgz#af8a08632465224ef4174a9f06308c3c2a1ebc8e" @@ -10439,6 +10453,11 @@ diff@^3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -13047,13 +13066,13 @@ fs-exists-sync@^0.1.0: resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= -fs-extra@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880" - integrity sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA= +fs-extra@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" + integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ== dependencies: graceful-fs "^4.1.2" - jsonfile "^3.0.0" + jsonfile "^4.0.0" universalify "^0.1.0" fs-extra@^0.30.0: @@ -13162,6 +13181,11 @@ fstream@^1.0.12: mkdirp ">=0.5 0" rimraf "2" +fsu@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834" + integrity sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A== + fullname@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/fullname/-/fullname-3.3.0.tgz#a08747d6921229610b8178b7614fce10cb185f5a" @@ -15857,7 +15881,7 @@ is-boolean-object@^1.0.0: resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M= -is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5: +is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -18403,6 +18427,11 @@ lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.2.0, lodash.iseq resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= +lodash.isfunction@^3.0.8, lodash.isfunction@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" + integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -18413,6 +18442,11 @@ lodash.isnumber@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -18635,7 +18669,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.11, lodash@4.17.13, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.5: +lodash@4.17.13, lodash@4.17.15, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.5: version "4.17.13" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA== @@ -18645,6 +18679,11 @@ lodash@^3.10.1, lodash@^3.3.1, lodash@~3.10.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= +lodash@^4.16.4: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + lodash@^4.17.12: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" @@ -19072,6 +19111,15 @@ md5.js@^1.3.4: hash-base "^3.0.0" inherits "^2.0.1" +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + mdast-add-list-metadata@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz#95e73640ce2fc1fa2dcb7ec443d09e2bfe7db4cf" @@ -19619,6 +19667,25 @@ mobx@^4.9.2: resolved "https://registry.yarnpkg.com/mobx/-/mobx-4.9.4.tgz#bb37a0e4e05f0b02be89ced9d23445cad73377ad" integrity sha512-RaEpydw7D1ebp1pdFHrEMZcLk4nALAZyHAroCPQpqLzuIXIxJpLmMIe5PUZwYHqvlcWL6DVqDYCANZpPOi9iXA== +mocha-junit-reporter@^1.23.1: + version "1.23.1" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.1.tgz#ba11519c0b967f404e4123dd69bc4ba022ab0f12" + integrity sha512-qeDvKlZyAH2YJE1vhryvjUQ06t2hcnwwu4k5Ddwn0GQINhgEYFhlGM0DwYCVUHq5cuo32qAW6HDsTHt7zz99Ng== + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + +mocha-multi-reporters@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" + integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= + dependencies: + debug "^3.1.0" + lodash "^4.16.4" + mocha@3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" @@ -19653,6 +19720,51 @@ mocha@^2.0.1, mocha@^2.3.4: supports-color "1.2.0" to-iso-string "0.0.2" +mochawesome-merge@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mochawesome-merge/-/mochawesome-merge-2.0.1.tgz#c690433acc78fd769effe4db1a107508351e2dc5" + integrity sha512-QRYok/9y9MJ4zlWGajC/OV6BxjUGyv1AYX3DBOPSbpzk09p2dFBWV1QYSN/dHu7bo/q44ZGmOBHO8ZnAyI+Yug== + dependencies: + fs-extra "^7.0.1" + minimatch "^3.0.4" + uuid "^3.3.2" + yargs "^12.0.5" + +mochawesome-report-generator@^4.0.0, mochawesome-report-generator@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mochawesome-report-generator/-/mochawesome-report-generator-4.0.1.tgz#0a010d1ecf379eb26ba05300feb59e2665076080" + integrity sha512-hQbmQt8/yCT68GjrQFat+Diqeuka3haNllexYfja1+y0hpwi3yCJwFpQCdWK9ezzcXL3Nu80f2I6SZeyspwsqg== + dependencies: + chalk "^2.4.2" + dateformat "^3.0.2" + fs-extra "^7.0.0" + fsu "^1.0.2" + lodash.isfunction "^3.0.8" + opener "^1.4.2" + prop-types "^15.7.2" + react "^16.8.5" + react-dom "^16.8.5" + tcomb "^3.2.17" + tcomb-validation "^3.3.0" + validator "^10.11.0" + yargs "^13.2.2" + +mochawesome@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mochawesome/-/mochawesome-4.0.1.tgz#351af69c8904468e75a71f8704ed0b5767795ccc" + integrity sha512-F/hVmiwWCvwBiW/UPhs4/lfgf8mBJBr89W/9fDu+hb+rQ9gFxWh9N/BU7RtEH+dMfBF4o8XIdYHrEcwxJhzqsw== + dependencies: + chalk "^2.4.1" + diff "^4.0.1" + json-stringify-safe "^5.0.1" + lodash.isempty "^4.4.0" + lodash.isfunction "^3.0.9" + lodash.isobject "^3.0.2" + lodash.isstring "^4.0.1" + mochawesome-report-generator "^4.0.0" + strip-ansi "^5.0.0" + uuid "^3.3.2" + module-definition@^3.0.0, module-definition@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-3.2.0.tgz#a1741d5ddf60d76c60d5b1f41ba8744ba08d3ef4" @@ -20046,11 +20158,6 @@ node-ensure@^0.0.0: resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc= -node-eta@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/node-eta/-/node-eta-0.1.1.tgz#4066109b39371c761c72b7ebda9a9ea0a5de121f" - integrity sha1-QGYQmzk3HHYccrfr2pqeoKXeEh8= - node-fetch@1.7.3, node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -20720,6 +20827,11 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +opener@^1.4.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" + integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== + opentracing@^0.13.0: version "0.13.0" resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.13.0.tgz#6a341442f09d7d866bc11ed03de1e3828e3d6aab" @@ -22910,7 +23022,7 @@ react-dom@^16.8.1: prop-types "^15.6.2" scheduler "^0.13.5" -react-dom@^16.8.3: +react-dom@^16.8.3, react-dom@^16.8.5: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== @@ -23526,7 +23638,7 @@ react@^16.8.1: prop-types "^15.6.2" scheduler "^0.13.5" -react@^16.8.3: +react@^16.8.3, react@^16.8.5: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== @@ -24241,13 +24353,12 @@ replace-ext@1.0.0, replace-ext@^1.0.0: resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= -request-progress@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.4.0.tgz#c1954e39086aa85269c5660bcee0142a6a70d7e7" - integrity sha1-wZVOOQhqqFJpxWYLzuAUKmpw1+c= +request-progress@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= dependencies: - node-eta "^0.1.1" - throttleit "^0.0.2" + throttleit "^1.0.0" request-promise-core@1.1.1: version "1.1.1" @@ -26831,6 +26942,18 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" +tcomb-validation@^3.3.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/tcomb-validation/-/tcomb-validation-3.4.1.tgz#a7696ec176ce56a081d9e019f8b732a5a8894b65" + integrity sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA== + dependencies: + tcomb "^3.0.0" + +tcomb@^3.0.0, tcomb@^3.2.17: + version "3.2.29" + resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-3.2.29.tgz#32404fe9456d90c2cf4798682d37439f1ccc386c" + integrity sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ== + tcp-port-used@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70" @@ -26984,10 +27107,10 @@ throat@^4.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= -throttleit@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" - integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8= +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= through2-filter@^2.0.0: version "2.0.0" @@ -28653,6 +28776,11 @@ validate-npm-package-name@2.2.2: dependencies: builtins "0.0.7" +validator@^10.11.0: + version "10.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" + integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== + validator@^8.0.0: version "8.2.0" resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9" @@ -29900,6 +30028,11 @@ xml2js@^0.4.19, xml2js@^0.4.5: sax ">=0.6.0" xmlbuilder "~9.0.1" +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= + xmlbuilder@8.2.2: version "8.2.2" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" @@ -30108,7 +30241,7 @@ yargs@^12.0.2: y18n "^3.2.1 || ^4.0.0" yargs-parser "^10.1.0" -yargs@^13.3.0: +yargs@^13.2.2, yargs@^13.3.0: version "13.3.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
{{detail.label}} +
{{detail.label}}
+
- {{detail.value}} - ({{detail.percent}}) +
{{detail.value}} ({{detail.percent}})