diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index b09de576f2d4a..ef21e1c5333e0 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -34,4 +34,4 @@ which indicates the next transaction in the trace. These transactions can be expanded and viewed in detail by clicking on them. After exploring these traces, -you can return to the full trace by clicking *View full trace* in the upper right hand corner of the page. +you can return to the full trace by clicking *View full trace*. diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 5c92afa55109d..1eb037009efff 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -105,6 +105,7 @@ image::apm/images/apm-transaction-duration-dist.png[Example view of transactions This graph shows a typical distribution, and indicates most of our requests were served quickly - awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. + When you select one of these buckets, you're presented with up to ten trace samples. Each sample has a span timeline waterfall that shows what a typical request in that bucket was doing. diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index ee29926914ad6..5cae3fcc7b531 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -76,7 +76,7 @@ After you've added the workpad to your website, you can change the autoplay and To change the autoplay settings: -. In the lower right corner of the shareable workpad, click the settings icon. +. Click the settings icon. . Click *Auto Play*, then change the settings. + @@ -85,7 +85,7 @@ image::images/canvas_share_autoplay_480.gif[Autoplay settings] To change the toolbar settings: -. In the lower right corner, click the settings icon. +. Click the settings icon. . Click *Toolbar*, then change the settings. + diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index b6d684bdf5dde..a38ab4a69598e 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -18,7 +18,7 @@ Your first step to working with Canvas is to create a workpad. . Click *Create workpad*. -. To add a *Name* for your workpad, use the editor on the right. For example, `My Canvas Workpad`. +. To add a *Name* for your workpad, use the editor. For example, `My Canvas Workpad`. [float] === Customize your workpad with images @@ -29,7 +29,7 @@ To customize your workpad to look the way you want, add your own images. + The default Elastic logo image appears on your page. -. To replace the Elastic logo with your own image, select the image, then use the editor on the right. +. To replace the Elastic logo with your own image, select the image, then use the editor. . To move the image, click and drag it to your preferred location. @@ -73,7 +73,7 @@ You'll notice that the error is gone, but the number could use some formatting. . To format the number, use the Canvas expression language. -.. In the lower right corner, click *Expression editor*. +.. Click *Expression editor*. + You're now looking at the raw data syntax that Canvas uses to display the element. diff --git a/docs/canvas/canvas-workpad.asciidoc b/docs/canvas/canvas-workpad.asciidoc index c5c163441439c..42eedf55c404d 100644 --- a/docs/canvas/canvas-workpad.asciidoc +++ b/docs/canvas/canvas-workpad.asciidoc @@ -124,7 +124,7 @@ Organize your ideas onto separate pages by adding more pages. . Click *Page 1*, then click *+*. -. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown. +. On the *Page* editor panel, select the page transition from the *Transition* dropdown. + [role="screenshot"] image::images/canvas-add-pages.gif[Add pages] diff --git a/docs/dev-tools/painlesslab/images/painless-lab.png b/docs/dev-tools/painlesslab/images/painless-lab.png new file mode 100644 index 0000000000000..f65257852792e Binary files /dev/null and b/docs/dev-tools/painlesslab/images/painless-lab.png differ diff --git a/docs/dev-tools/painlesslab/index.asciidoc b/docs/dev-tools/painlesslab/index.asciidoc new file mode 100644 index 0000000000000..e55424baf3142 --- /dev/null +++ b/docs/dev-tools/painlesslab/index.asciidoc @@ -0,0 +1,17 @@ +[role="xpack"] +[[painlesslab]] +== Painless Lab + +beta::[] + +The Painless Lab is an interactive code editor that lets you test and +debug {ref}/modules-scripting-painless.html[Painless scripts] in real-time. +You can use the Painless scripting +language to create <>, +process {ref}/docs-reindex.html[reindexed data], define complex +<>, +and work with data in other contexts. + +To get started, go to *Dev Tools > Painless Lab*. + +image::dev-tools/painlesslab/images/painless-lab.png[Painless Lab] diff --git a/docs/management/alerting/alert-management.asciidoc b/docs/management/alerting/alert-management.asciidoc index caf260937b7be..73cf40c4d7c40 100644 --- a/docs/management/alerting/alert-management.asciidoc +++ b/docs/management/alerting/alert-management.asciidoc @@ -18,9 +18,9 @@ For more information on alerting concepts and the types of alerts and actions av [float] ==== Finding alerts -The *Alerts* tab lists all alerts in the current space, including summary information about their execution frequency, tags, and type. +The *Alerts* tab lists all alerts in the current space, including summary information about their execution frequency, tags, and type. -The *search bar* can be used to quickly find alerts by name or tag. +The *search bar* can be used to quickly find alerts by name or tag. [role="screenshot"] image::images/alerts-filter-by-search.png[Filtering the alerts list using the search bar] @@ -30,7 +30,7 @@ The *type* dropdown lets you filter to a subset of alert types. [role="screenshot"] image::images/alerts-filter-by-type.png[Filtering the alerts list by types of alert] -The *Action type* dropdown lets you filter by the type of action used in the alert. +The *Action type* dropdown lets you filter by the type of action used in the alert. [role="screenshot"] image::images/alerts-filter-by-action-type.png[Filtering the alert list by type of action] @@ -39,16 +39,16 @@ image::images/alerts-filter-by-action-type.png[Filtering the alert list by type [[create-edit-alerts]] ==== Creating and editing alerts -Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. +Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. -After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. +After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. [float] [[controlling-alerts]] ==== Controlling alerts -The alert listing allows you to quickly mute/unmute, disable/enable, and delete individual alerts by clicking the action button at the right of each row. +The alert listing allows you to quickly mute/unmute, disable/enable, and delete individual alerts by clicking the action button. [role="screenshot"] image:management/alerting/images/individual-mute-disable.png[The actions button allows an individual alert to be muted, disabled, or deleted] @@ -56,4 +56,4 @@ image:management/alerting/images/individual-mute-disable.png[The actions button These operations can also be performed in bulk by multi-selecting alerts and clicking the *Manage alerts* button: [role="screenshot"] -image:management/alerting/images/bulk-mute-disable.png[The Manage alerts button lets you mute/unmute, enable/disable, and delete in bulk] \ No newline at end of file +image:management/alerting/images/bulk-mute-disable.png[The Manage alerts button lets you mute/unmute, enable/disable, and delete in bulk] diff --git a/docs/management/alerting/connector-management.asciidoc b/docs/management/alerting/connector-management.asciidoc index 1002a372f9460..46e106e6e9648 100644 --- a/docs/management/alerting/connector-management.asciidoc +++ b/docs/management/alerting/connector-management.asciidoc @@ -15,7 +15,7 @@ image::images/connector-listing.png[Example connector listing in the Alerts and [float] ==== Connector list -The *Connectors* tab lists all connectors in the current space. The *search bar* can be used to find specific connectors by name and/or type. +The *Connectors* tab lists all connectors in the current space. The *search bar* can be used to find specific connectors by name and/or type. [role="screenshot"] image::images/connector-filter-by-search.png[Filtering the connector list using the search bar] @@ -26,12 +26,12 @@ The *type* dropdown also lets you filter to a subset of action types. [role="screenshot"] image::images/connector-filter-by-type.png[Filtering the connector list by types of actions] -The *Actions* column indicates the number of actions that reference the connector. This count helps you confirm a connector is unused before you delete it, and tells you how many actions will be affected when a connector is modified. +The *Actions* column indicates the number of actions that reference the connector. This count helps you confirm a connector is unused before you delete it, and tells you how many actions will be affected when a connector is modified. [role="screenshot"] image::images/connector-action-count.png[Filtering the connector list by types of actions] -You can delete individual connectors using the trash icon on the right of each row. Connectors can also be deleted in bulk by multi-selecting them and clicking the *Delete* button to the left of the search box. +You can delete individual connectors using the trash icon. Connectors can also be deleted in bulk by multi-selecting them and clicking the *Delete* button to the left of the search box. [role="screenshot"] image::images/connector-delete.png[Deleting connectors individually or in bulk] @@ -44,4 +44,4 @@ When this happens the action will fail to execute, and appear as errors in the { ==== Creating a new connector -New connectors can be created by clicking the *Create connector* button, which will guide you to select the type of connector and configure it's properties. Refer to <> for the types of connectors available and how to configure them. Once you create a connector it will be made available to you anytime you set up an action in the current space. \ No newline at end of file +New connectors can be created by clicking the *Create connector* button, which will guide you to select the type of connector and configure it's properties. Refer to <> for the types of connectors available and how to configure them. Once you create a connector it will be made available to you anytime you set up an action in the current space. diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 45f8bd13a5c54..bb16faab7fe5a 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -38,7 +38,6 @@ image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollu Just start typing in the *Index pattern* field, and {kib} looks for the names of {es} indices that match your input. Make sure that the name of the index pattern is unique. -To include system indices in your search, toggle the switch in the upper right. [role="screenshot"] image:management/index-patterns/images/create-index-pattern.png["Create index pattern"] diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 1a1bcec10ab50..9682d918aabe4 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -25,7 +25,7 @@ the *Index patterns* overview. [role="screenshot"] image::management/index-patterns/images/new-index-pattern.png["Index files and data types"] -Use the icons in the upper right to perform the following actions: +Use the icons to perform the following actions: * [[set-default-pattern]]*Set the default index pattern.* {kib} uses a badge to make users aware of which index pattern is the default. The first pattern diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 6a56970687fd6..da2e190847fdb 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -42,8 +42,8 @@ image::images/management_create_rollup_job.png[][Wizard that walks you through c === Start, stop, and delete rollup jobs Once you’ve saved a rollup job, you’ll see it the *Rollup Jobs* overview page, -where you can drill down for further investigation. The *Manage* menu in -the lower right enables you to start, stop, and delete the rollup job. +where you can drill down for further investigation. The *Manage* menu enables +you to start, stop, and delete the rollup job. You must first stop a rollup job before deleting it. [role="screenshot"] diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc index ad20264f56138..7e2cdddfd30ef 100644 --- a/docs/maps/geojson-upload.asciidoc +++ b/docs/maps/geojson-upload.asciidoc @@ -37,7 +37,6 @@ the Elasticsearch responses are shown on the *Layer add panel* and the indexed d appears on the map. The geospatial data on the map should be identical to the locally-previewed data, but now it's indexed data from Elasticsearch. -. To continue adding data to the map, click *Add layer* in the lower -right-hand corner. +. To continue adding data to the map, click *Add layer*. . In *Layer settings*, adjust any settings or <> as needed. . Click *Save & close*. diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index a94e5757d5dfa..bf846a2b80e03 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -55,14 +55,14 @@ auto-populate *Index type* with either {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape] and *Index name* with ``. -. Click *Import file* in the lower right. +. Click *Import file*. + You'll see activity as the GeoJSON Upload utility creates a new index and index pattern for the data set. When the process is complete, you should receive messages that the creation of the new index and index pattern were successful. -. Click *Add layer* in the bottom right. +. Click *Add layer*. . In *Layer settings*, adjust settings and <> as needed. . Click *Save & close*. diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index b13eeebe56fd8..6495b8a057cf6 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -80,7 +80,7 @@ To symbolize countries by web traffic, you'll need to augment the world country To do this, you'll create a <> to link the vector source *World Countries* to the {es} index `kibana_sample_data_logs` on the shared key iso2 = geo.src. -. Click plus image:maps/images/gs_plus_icon.png[] to the right of *Term Joins* label. +. Click plus image:maps/images/gs_plus_icon.png[] next to the *Term Joins* label. . Click *Join --select--* . Set *Left field* to *ISO 3166-1 alpha-2 code*. . Set *Right source* to *kibana_sample_data_logs*. @@ -238,7 +238,7 @@ The *machine.os.keyword: osx* filter appears in the dashboard query bar. + . Click the *x* to remove the *machine.os.keyword: osx* filter. . In the map, click in the United States vector. -. Click plus image:maps/images/gs_plus_icon.png[] to the right of *iso2* row in the tooltip. +. Click plus image:maps/images/gs_plus_icon.png[] next to the *iso2* row in the tooltip. + Both the visualizations and the map are filtered to only show documents where *geo.src* is *US*. The *geo.src: US* filter appears in the dashboard query bar. @@ -247,4 +247,3 @@ Your dashboard should look like this: + [role="screenshot"] image::maps/images/gs_dashboard_with_terms_filter.png[] - diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 8a93352798d2c..a461ab6fbb3a6 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -4,7 +4,7 @@ **Elastic Maps** embeds the search bar for real-time search. Only layers requesting data from {es} are filtered when you submit a search request. -Layers narrowed by the search context contain the filter icon image:maps/images/filter_icon.png[] to the right of layer name in the legend. +Layers narrowed by the search context contain the filter icon image:maps/images/filter_icon.png[] next to the layer name in the legend. You can create a layer that requests data from {es} from the following: diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 89c4c88708d58..f05afac34e595 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -2,7 +2,7 @@ [[defining-alerts]] == Defining alerts -{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. +{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] === Alert flyout @@ -25,20 +25,20 @@ All alert share the following four properties in common: image::images/alert-flyout-general-details.png[All alerts have name, tags, check every, and re-notify every properties in common] Name:: The name of the alert. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable alert listing in the management UI. A distinctive name can help identify and find an alert. -Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. +Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. -Re-notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. +Re-notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. [float] [[defining-alerts-type-conditions]] === Alert type and conditions -Depending upon the {kib} app and context, you may be prompted to choose the type of alert you wish to create. Some apps will pre-select the type of alert for you. +Depending upon the {kib} app and context, you may be prompted to choose the type of alert you wish to create. Some apps will pre-select the type of alert for you. [role="screenshot"] image::images/alert-flyout-alert-type-selection.png[Choosing the type of alert to create] -Each alert type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold alert the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. +Each alert type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold alert the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. [role="screenshot"] image::images/alert-flyout-alert-conditions.png[UI for defining alert conditions on an index threshold alert] @@ -52,19 +52,19 @@ To add an action to an alert, you first select the type of action: [role="screenshot"] image::images/alert-flyout-action-type-selection.png[UI for selecting an action type] -Each action must specify a <> instance. If no connectors exist for that action type, click "Add new" to create one. +Each action must specify a <> instance. If no connectors exist for that action type, click "Add new" to create one. -Each action type exposes different properties. For example an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. +Each action type exposes different properties. For example an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. [role="screenshot"] image::images/alert-flyout-action-details.png[UI for defining an email action] -Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Available variables differ by alert type, and a list can be accessed using the "add variable" button at the right of the text box. +Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Available variables differ by alert type, and a list can be accessed using the "add variable" button. [role="screenshot"] image::images/alert-flyout-action-variables.png[Passing alert values to an action] -You can attach more than one action. Clicking the "Add action" button will prompt you to select another alert type and repeat the above steps again. +You can attach more than one action. Clicking the "Add action" button will prompt you to select another alert type and repeat the above steps again. [role="screenshot"] image::images/alert-flyout-add-action.png[You can add multiple actions on an alert] @@ -77,4 +77,4 @@ Actions are not required on alerts. In some cases you may want to run an alert w [float] === Managing alerts -To modify an alert after it was created, including muting or disabling it, use the <>. \ No newline at end of file +To modify an alert after it was created, including muting or disabling it, use the <>. diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index a17e46c5b3542..ab529a533d5e3 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -90,7 +90,7 @@ In *Edit* mode, you can move, resize, customize, and delete panels to suit your * To move a panel, click and hold the panel header and drag to the new location. [[resizing-containers]] -* To resize a panel, click the resize control on the lower right and drag +* To resize a panel, click the resize control and drag to the new dimensions. * To toggle the use of margins and panel titles, use the *Options* menu. diff --git a/docs/user/dev-tools.asciidoc b/docs/user/dev-tools.asciidoc index 77a781fd069e4..0ee7fbc741e00 100644 --- a/docs/user/dev-tools.asciidoc +++ b/docs/user/dev-tools.asciidoc @@ -4,13 +4,29 @@ [partintro] -- -The *Dev Tools* page contains development tools that you can use to interact -with your data in Kibana. +*Dev Tools* contains tools that you can use to interact +with your data. -* <> -* <> -* <> +[cols="2"] +|=== +a| <> + +| Interact with the REST API of Elasticsearch, including sending requests +and viewing API documentation. + +a| <> + +| Inspect and analyze your search queries. + +a| <> + +| Build and debug grok patterns before you use them in your data processing pipelines. + +a| <> + +| beta:[] Test and debug Painless scripts in real-time. +|=== -- @@ -19,3 +35,5 @@ include::{kib-repo-dir}/dev-tools/console/console.asciidoc[] include::{kib-repo-dir}/dev-tools/searchprofiler/index.asciidoc[] include::{kib-repo-dir}/dev-tools/grokdebugger/index.asciidoc[] + +include::{kib-repo-dir}/dev-tools/painlesslab/index.asciidoc[] diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 4222ba40debb7..2547b38a22616 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -33,7 +33,7 @@ which has a pre-built index pattern. By default, *Discover* shows data for the last 15 minutes. If you have a time-based index, and no data displays, -you might need to increase the time range. Using the <> in the upper right, +you might need to increase the time range. Using the <>, you can specify a common or recently-used time range, a relative time from now, or an absolute time range. diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index 1749678ace9e3..a155017f1bb22 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -38,7 +38,7 @@ image::user/graph/images/graph-url-connections.png["URL connections"] [role="screenshot"] image::user/graph/images/graph-link-summary.png["Link summary"] -. Use the control bar on the right to explore +. Use the control bar to explore additional connections: + * To display additional vertices that connect to your graph, click the expand icon @@ -70,7 +70,7 @@ select *Edit settings*. To change the color and label of selected vertices, click the style icon image:user/graph/images/graph-style-button.png[Style] -in the control bar on the right. +in the control bar. [float] diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index 0b2be4dd9e3d9..f4ecb2a74d91e 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -14,8 +14,8 @@ image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="imag To view an overview of the Beats data in the cluster, click *Overview*. The overview page has a section for activity in the last day, which is a real-time sample of data. The summary bar and charts follow the typical paradigm -of data in the Monitoring UI, which is bound to the span of the time filter in -the top right corner of the page. This overview page can therefore show +of data in the Monitoring UI, which is bound to the span of the time filter. +This overview page can therefore show up-to-date or historical information. To view a listing of the individual Beat instances in the cluster, click *Beats*. diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index 35570ea7ca1dc..b181763c0d0d0 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -38,7 +38,7 @@ you'll see two places highlighted in green: * The visualization builder pane -* The *X-axis* or *Y-axis* fields in the right column +* The *X-axis* or *Y-axis* fields You can incorporate many fields into your visualization, and Lens uses heuristics to decide how to apply each one to the visualization. @@ -89,8 +89,8 @@ You can switch between suggestions without losing your previous state: [role="screenshot"] image::images/lens_suggestions.gif[] -If you want to switch to a chart type that is not suggested, click the chart type in the -top right, then select a chart type. When there is an exclamation point (!) +If you want to switch to a chart type that is not suggested, click the chart type, +then select a chart type. When there is an exclamation point (!) next to a chart type, Lens is unable to transfer your current data, but still allows you to make the change. @@ -106,7 +106,7 @@ If there is a match, Lens displays the new data. All fields that do not match th . Change the data field options, such as the aggregation or label. -.. Click *Drop a field here* or the field name in the right column. +.. Click *Drop a field here* or the field name in the column. .. Change the options that appear depending on the type of field. @@ -168,7 +168,7 @@ image::images/lens_tutorial_2.png[Lens tutorial] Customize your visualization to look exactly how you want. -. In the right column, click *Average of taxful_total_price*. +. Click *Average of taxful_total_price*. .. Change the *Label* to `Sales`, or a name that you prefer for the data. @@ -180,7 +180,7 @@ six available categories. . Look at the suggestions. None of them show an area chart, but for sales data, a stacked area chart might make sense. To switch the chart type: -.. Click *Stacked bar chart* in the right column. +.. Click *Stacked bar chart* in the column. .. Click *Stacked area*. + diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js index f93dee14d0eed..8607ff184dfaa 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js @@ -49,6 +49,12 @@ const metricAggs = [ }), value: 'filter_ratio', }, + { + label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.positiveRateLabel', { + defaultMessage: 'Positive Rate', + }), + value: 'positive_rate', + }, { label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.maxLabel', { defaultMessage: 'Max', diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js new file mode 100644 index 0000000000000..39558fa3a9224 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_rate.js @@ -0,0 +1,191 @@ +/* + * 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 PropTypes from 'prop-types'; +import React from 'react'; +import { AggSelect } from './agg_select'; +import { FieldSelect } from './field_select'; +import { AggRow } from './agg_row'; +import { createChangeHandler } from '../lib/create_change_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { + htmlIdGenerator, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiFormRow, + EuiSpacer, + EuiText, + EuiLink, + EuiComboBox, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; + +const UNIT_OPTIONS = [ + { + label: i18n.translate('visTypeTimeseries.units.auto', { defaultMessage: 'auto' }), + value: '', + }, + { + label: i18n.translate('visTypeTimeseries.units.perMillisecond', { + defaultMessage: 'per millisecond', + }), + value: '1ms', + }, + { + label: i18n.translate('visTypeTimeseries.units.perSecond', { defaultMessage: 'per second' }), + value: '1s', + }, + { + label: i18n.translate('visTypeTimeseries.units.perMinute', { defaultMessage: 'per minute' }), + value: '1m', + }, + { + label: i18n.translate('visTypeTimeseries.units.perHour', { defaultMessage: 'per hour' }), + value: '1h', + }, + { + label: i18n.translate('visTypeTimeseries.units.perDay', { defaultMessage: 'per day' }), + value: '1d', + }, +]; + +export const PositiveRateAgg = props => { + const defaults = { unit: '' }; + const model = { ...defaults, ...props.model }; + + const handleChange = createChangeHandler(props.onChange, model); + const handleSelectChange = createSelectHandler(handleChange); + + const htmlId = htmlIdGenerator(); + const indexPattern = + (props.series.override_index_pattern && props.series.series_index_pattern) || + props.panel.index_pattern; + + const selectedUnitOptions = UNIT_OPTIONS.filter(o => o.value === model.unit); + + return ( + + + + + + + + + + + + } + fullWidth + > + + + + + + } + fullWidth + > + + + + + + +

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

+
+
+ ); +}; + +PositiveRateAgg.propTypes = { + disableDelete: PropTypes.bool, + fields: PropTypes.object, + model: PropTypes.object, + onAdd: PropTypes.func, + onChange: PropTypes.func, + onDelete: PropTypes.func, + panel: PropTypes.object, + series: PropTypes.object, + siblings: PropTypes.array, +}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js index ca40d60f20848..a53192afafdcc 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js @@ -33,6 +33,7 @@ import { PercentileRankAgg } from '../aggs/percentile_rank'; import { Static } from '../aggs/static'; import { MathAgg } from '../aggs/math'; import { TopHitAgg } from '../aggs/top_hit'; +import { PositiveRateAgg } from '../aggs/positive_rate'; export const aggToComponent = { count: StandardAgg, @@ -65,4 +66,5 @@ export const aggToComponent = { static: Static, math: MathAgg, top_hit: TopHitAgg, + positive_rate: PositiveRateAgg, }; diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.js b/src/plugins/vis_type_timeseries/common/agg_lookup.js index 4dfdc83dcfabb..432da03e3d45d 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.js @@ -97,6 +97,9 @@ export const lookup = { defaultMessage: 'Static Value', }), top_hit: i18n.translate('visTypeTimeseries.aggLookup.topHitLabel', { defaultMessage: 'Top Hit' }), + positive_rate: i18n.translate('visTypeTimeseries.aggLookup.positiveRateLabel', { + defaultMessage: 'Positive Rate', + }), }; const pipeline = [ diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.js b/src/plugins/vis_type_timeseries/common/calculate_label.js index 756d6e57a83e8..71aa0aed7dc11 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.js @@ -70,6 +70,12 @@ export function calculateLabel(metric, metrics) { defaultMessage: 'Filter Ratio', }); } + if (metric.type === 'positive_rate') { + return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', { + defaultMessage: 'Positive Rate of {field}', + values: { field: metric.field }, + }); + } if (metric.type === 'static') { return i18n.translate('visTypeTimeseries.calculateLabel.staticValueLabel', { defaultMessage: 'Static Value of {metricValue}', diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js index 4b0b8f33716a2..c727a3131f5df 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js @@ -26,6 +26,7 @@ import { dateHistogram } from './date_histogram'; import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; +import { positiveRate } from './positive_rate'; import { normalizeQuery } from './normalize_query'; export const processors = [ @@ -38,5 +39,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + positiveRate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js new file mode 100644 index 0000000000000..1ff548cc19e02 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -0,0 +1,68 @@ +/* + * 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 { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { set } from 'lodash'; + +export const filter = metric => metric.type === 'positive_rate'; + +export const createPositiveRate = (doc, intervalString, aggRoot) => metric => { + const maxFn = bucketTransform.max; + const derivativeFn = bucketTransform.derivative; + const positiveOnlyFn = bucketTransform.positive_only; + + const maxMetric = { id: `${metric.id}-positive-rate-max`, type: 'max', field: metric.field }; + const derivativeMetric = { + id: `${metric.id}-positive-rate-derivative`, + type: 'derivative', + field: `${metric.id}-positive-rate-max`, + unit: metric.unit, + }; + const positiveOnlyMetric = { + id: metric.id, + type: 'positive_only', + field: `${metric.id}-positive-rate-derivative`, + }; + + const fakeSeriesMetrics = [maxMetric, derivativeMetric, positiveOnlyMetric]; + + const maxBucket = maxFn(maxMetric, fakeSeriesMetrics, intervalString); + const derivativeBucket = derivativeFn(derivativeMetric, fakeSeriesMetrics, intervalString); + const positiveOnlyBucket = positiveOnlyFn(positiveOnlyMetric, fakeSeriesMetrics, intervalString); + + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-max`, maxBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-derivative`, derivativeBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, positiveOnlyBucket); +}; + +export function positiveRate(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { intervalString } = getBucketSize(req, interval, capabilities); + if (series.metrics.some(filter)) { + series.metrics + .filter(filter) + .forEach(createPositiveRate(doc, intervalString, `aggs.${series.id}.aggs`)); + return next(doc); + } + return next(doc); + }; +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js new file mode 100644 index 0000000000000..946884c05c722 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js @@ -0,0 +1,95 @@ +/* + * 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 { positiveRate } from './positive_rate'; +describe('positiveRate(req, panel, series)', () => { + let panel; + let series; + let req; + beforeEach(() => { + panel = { + time_field: 'timestamp', + }; + series = { + id: 'test', + split_mode: 'terms', + terms_size: 10, + terms_field: 'host', + metrics: [ + { + id: 'metric-1', + type: 'positive_rate', + field: 'system.network.out.bytes', + unit: '1s', + }, + ], + }; + req = { + payload: { + timerange: { + min: '2017-01-01T00:00:00Z', + max: '2017-01-01T01:00:00Z', + }, + }, + }; + }); + + test('calls next when finished', () => { + const next = jest.fn(); + positiveRate(req, panel, series)(next)({}); + expect(next.mock.calls.length).toEqual(1); + }); + + test('returns positive rate aggs', () => { + const next = doc => doc; + const doc = positiveRate(req, panel, series)(next)({}); + expect(doc).toEqual({ + aggs: { + test: { + aggs: { + timeseries: { + aggs: { + 'metric-1-positive-rate-max': { + max: { field: 'system.network.out.bytes' }, + }, + 'metric-1-positive-rate-derivative': { + derivative: { + buckets_path: 'metric-1-positive-rate-max', + gap_policy: 'skip', + unit: '1s', + }, + }, + 'metric-1': { + bucket_script: { + buckets_path: { value: 'metric-1-positive-rate-derivative[normalized_value]' }, + script: { + source: 'params.value > 0.0 ? params.value : 0.0', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js index a62533ae7a37c..5864d2538005d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js @@ -26,6 +26,7 @@ import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; import { normalizeQuery } from './normalize_query'; +import { positiveRate } from './positive_rate'; export const processors = [ query, @@ -36,5 +37,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + positiveRate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js new file mode 100644 index 0000000000000..da4b834822d70 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js @@ -0,0 +1,35 @@ +/* + * 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 { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { calculateAggRoot } from './calculate_agg_root'; +import { createPositiveRate, filter } from '../series/positive_rate'; + +export function positiveRate(req, panel, esQueryConfig, indexPatternObject) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { intervalString } = getBucketSize(req, interval); + panel.series.forEach(column => { + const aggRoot = calculateAggRoot(doc, column); + column.metrics.filter(filter).forEach(createPositiveRate(doc, intervalString, aggRoot)); + }); + return next(doc); + }; +} diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index efc0dad394464..2c927e9a2f4c7 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -52,6 +52,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { describe('Kibana browser back navigation should work', function describeIndexTests() { before(async () => { + await esArchiver.loadIfNeeded('discover'); await esArchiver.loadIfNeeded('logstash_functional'); if (browser.isInternetExplorer) { await kibanaServer.uiSettings.replace({ 'state:storeInSessionStorage': false }); diff --git a/x-pack/legacy/plugins/canvas/scripts/jest.js b/x-pack/legacy/plugins/canvas/scripts/jest.js index cce1b8d355846..133f775c7192f 100644 --- a/x-pack/legacy/plugins/canvas/scripts/jest.js +++ b/x-pack/legacy/plugins/canvas/scripts/jest.js @@ -36,6 +36,14 @@ run( `!${path}/**/__tests__/**/*`, '--collectCoverageFrom', // Ignore coverage on example files `!${path}/**/__examples__/**/*`, + '--collectCoverageFrom', // Ignore flot files + `!${path}/**/flot-charts/**`, + '--collectCoverageFrom', // Ignore coverage files + `!${path}/**/coverage/**`, + '--collectCoverageFrom', // Ignore scripts + `!${path}/**/scripts/**`, + '--collectCoverageFrom', // Ignore mock files + `!${path}/**/mocks/**`, '--collectCoverageFrom', // Include JS files `${path}/**/*.js`, '--collectCoverageFrom', // Include TS/X files @@ -76,7 +84,7 @@ run( --all Runs all tests and snapshots. Slower. --storybook Runs Storybook Snapshot tests only. --update Updates Storybook Snapshot tests. - --path Runs any tests at a given path. + --path Runs any tests at a given path. --coverage Collect coverage statistics. `, }, diff --git a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap b/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap index 757677f1d4f82..3ae3079da136b 100644 --- a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap +++ b/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap @@ -66,6 +66,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -162,6 +163,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "index": ".reporting", @@ -257,6 +259,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -353,6 +356,7 @@ Object { "duration": "30s", "size": 500, }, + "useByteOrderMarkEncoding": false, }, "enabled": true, "index": ".reporting", diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index 8f7a06ba9f8e9..e3d6a4274e7df 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -19,6 +19,7 @@ export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; +export const CSV_BOM_CHARS = '\ufeff'; export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/json', diff --git a/x-pack/legacy/plugins/reporting/config.ts b/x-pack/legacy/plugins/reporting/config.ts index 211fa70301bbf..5eceb84c83e43 100644 --- a/x-pack/legacy/plugins/reporting/config.ts +++ b/x-pack/legacy/plugins/reporting/config.ts @@ -135,6 +135,7 @@ export async function config(Joi: any) { .default(), }).default(), csv: Joi.object({ + useByteOrderMarkEncoding: Joi.boolean().default(false), checkForFormulas: Joi.boolean().default(true), enablePanelActionDownload: Joi.boolean().default(true), maxSizeBytes: Joi.number() diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js index 93dbe598b367c..4870e1e35cdaf 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js @@ -13,6 +13,7 @@ import { createMockReportingCore } from '../../../test_helpers'; import { LevelLogger } from '../../../server/lib/level_logger'; import { setFieldFormats } from '../../../server/services'; import { executeJobFactory } from './execute_job'; +import { CSV_BOM_CHARS } from '../../../common/constants'; const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); @@ -374,6 +375,50 @@ describe('CSV Execute Job', function() { }); }); + describe('Byte order mark encoding', () => { + it('encodes CSVs with BOM', async () => { + configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'one', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = { + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }; + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual(`${CSV_BOM_CHARS}one,two\none,bar\n`); + }); + + it('encodes CSVs without BOM', async () => { + configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(false); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'one', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = { + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }; + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual('one,two\none,bar\n'); + }); + }); + describe('Elasticsearch call errors', function() { it('should reject Promise if search call errors out', async function() { callAsCurrentUserStub.rejects(new Error()); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 3a282eb0b2974..376a398da274f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; import { IUiSettingsClient, KibanaRequest } from '../../../../../../../src/core/server'; -import { CSV_JOB_TYPE } from '../../../common/constants'; +import { CSV_JOB_TYPE, CSV_BOM_CHARS } from '../../../common/constants'; import { ReportingCore } from '../../../server/core'; import { cryptoFactory } from '../../../server/lib'; import { getFieldFormats } from '../../../server/services'; @@ -121,6 +121,8 @@ export const executeJobFactory: ExecuteJobFactory => + Promise.resolve(connectorsMock); + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise => + Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts new file mode 100644 index 0000000000000..ef0e51fb1c24d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { KibanaServices } from '../../../lib/kibana'; +import { fetchConnectors, getCaseConfigure, postCaseConfigure, patchCaseConfigure } from './api'; +import { + connectorsMock, + caseConfigurationMock, + caseConfigurationResposeMock, + caseConfigurationCamelCaseResponseMock, +} from './mock'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('fetch connectors', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(connectorsMock); + }); + + test('check url, method, signal', async () => { + await fetchConnectors({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/connectors/_find', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await fetchConnectors({ signal: abortCtrl.signal }); + expect(resp).toEqual(connectorsMock); + }); + }); + + describe('fetch configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, method, signal', async () => { + await getCaseConfigure({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + + test('return null on empty response', async () => { + fetchMock.mockResolvedValue({}); + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toBe(null); + }); + }); + + describe('create configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: + '{"connector_id":"123","connector_name":"My Connector","closure_type":"close-by-user"}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); + + describe('update configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await patchCaseConfigure({ connector_id: '456', version: 'WzHJ12' }, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: '{"connector_id":"456","version":"WzHJ12"}', + method: 'PATCH', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCaseConfigure( + { connector_id: '456', version: 'WzHJ12' }, + abortCtrl.signal + ); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts new file mode 100644 index 0000000000000..d2491b39fdf56 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Connector, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../../../plugins/case/common/api'; +import { CaseConfigure } from './types'; + +export const connectorsMock: Connector[] = [ + { + id: '123', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: true, + }, + { + id: '456', + actionTypeId: '.servicenow', + name: 'My Connector 2', + config: { + apiUrl: 'https://instance2.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: true, + }, +]; + +export const caseConfigurationResposeMock: CasesConfigureResponse = { + created_at: '2020-04-06T13:03:18.657Z', + created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', + updated_at: '2020-04-06T14:03:18.657Z', + updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; + +export const caseConfigurationMock: CasesConfigureRequest = { + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', +}; + +export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { + createdAt: '2020-04-06T13:03:18.657Z', + createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + connectorId: '123', + connectorName: 'My Connector', + closureType: 'close-by-user', + updatedAt: '2020-04-06T14:03:18.657Z', + updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx new file mode 100644 index 0000000000000..3ee16e19eaf9f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx @@ -0,0 +1,299 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useCaseConfigure, ReturnUseCaseConfigure, PersistCaseConfigure } from './use_configure'; +import { caseConfigurationCamelCaseResponseMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +const configuration: PersistCaseConfigure = { + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-pushing', +}; + +describe('useConfigure', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const args = { + setConnector: jest.fn(), + setClosureType: jest.fn(), + setCurrentConfiguration: jest.fn(), + }; + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('fetch case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('fetch case configuration - setConnector', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setConnector).toHaveBeenCalledWith('123', 'My Connector'); + }); + }); + + test('fetch case configuration - setClosureType', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setClosureType).toHaveBeenCalledWith('close-by-user'); + }); + }); + + test('fetch case configuration - setCurrentConfiguration', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setCurrentConfiguration).toHaveBeenCalledWith({ + connectorId: '123', + closureType: 'close-by-user', + }); + }); + }); + + test('fetch case configuration - only setConnector', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure({ setConnector: jest.fn() }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('refetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when fetching case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('persist case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current).toEqual({ + loading: false, + persistLoading: true, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('save case configuration - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); + expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); + expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { + connectorId: '456', + closureType: 'close-by-pushing', + }); + }); + }); + + test('save case configuration - patchCaseConfigure', async () => { + const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); + spyOnPatchCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); + expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); + expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { + connectorId: '456', + closureType: 'close-by-pushing', + }); + }); + }); + + test('save case configuration - only setConnector', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure({ setConnector: jest.fn() }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('unhappy path - fetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('unhappy path - persist case configuration', async () => { + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure(args) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx index 19d80bba1e0f8..7f57149d4e56d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -12,7 +12,7 @@ import * as i18n from './translations'; import { ClosureType } from './types'; import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; -interface PersistCaseConfigure { +export interface PersistCaseConfigure { connectorId: string; connectorName: string; closureType: ClosureType; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx new file mode 100644 index 0000000000000..0d6b6acfd9065 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useConnectors, ReturnConnectors } from './use_connectors'; +import { connectorsMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useConnectors', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('fetch connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + connectors: connectorsMock, + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('refetch connectors', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + expect(spyOnfetchConnectors).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when refetching connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('unhappy path', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + spyOnfetchConnectors.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 5156a6146f3a1..9c51139993b3f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -10,6 +10,7 @@ import { HealthCheck } from './health_check'; import { act } from 'react-dom/test-utils'; import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import '@testing-library/jest-dom/extend-expect'; const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index 6c34556840960..de3976509be1f 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,7 +19,8 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - describe('custom elements', function() { + // FLAKY: https://github.com/elastic/kibana/issues/62927 + describe.skip('custom elements', function() { this.tags('skipFirefox'); before(async () => { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index c2013ba3502e2..b5bcd33c3b9ab 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -13,12 +13,20 @@ function generateUniqueKey() { } export default ({ getPageObjects, getService }: FtrProviderContext) => { + const alerting = getService('alerting'); const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const find = getService('find'); describe('Connectors', function() { before(async () => { + await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('connectorsTab'); });