diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index 7b771eb662616..8aac22c742433 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -2,42 +2,63 @@ [[advanced-queries]] === Query your data -Querying your APM data is a powerful tool that can make finding bottlenecks in your code even easier. -Imagine you have a user that complains about a slow response time in a specific service. -With the query bar, you can easily filter the APM app to only display trace data for that user, -or, to only show transactions that are slower than a specified time threshold. +Querying your APM data is an essential tool that can make finding bottlenecks in your code even more straightforward. -[float] -==== Example APM app queries +Using the query bar, a powerful data query feature, you can pass advanced queries on your data +to filter on specific pieces of information you’re interested in. + +The query bar comes with a handy autocomplete that helps find the fields and even provides suggestions to the data they include. +You can select the query bar and hit the down arrow on your keyboard to begin scanning recommendations. -* Exclude response times slower than 2000 ms: `transaction.duration.us > 2000000` -* Filter by response status code: `context.response.status_code ≥ 400` -* Filter by single user ID: `context.user.id : 12` +[float] +[[apm-app-advanced-queries]] +=== Querying in the APM app -When querying in the APM app, you're merely searching and selecting data from fields in Elasticsearch documents. -Queries entered into the query bar are also added as parameters to the URL, -so it's easy to share a specific query or view with others. +When querying in the APM app, you’re merely searching and selecting data from fields in {es} documents. Queries entered +into the query bar are also added as parameters to the URL, so it’s easy to share a specific query or view with others. When you type, you can begin to see some of the transaction fields available for filtering: [role="screenshot"] image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] -TIP: Read the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation to learn more about the capabilities of the {kib} query language. +[TIP] +===== +To learn more about the {kib} query language capabilities, see the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation. +===== + +[float] +[[apm-app-queries]] +==== APM app queries + +APM queries can be handy for removing noise from your data in the <>, <>, +<>, <>, and <> views. + +For example, in the *Services* view, you can quickly view a list of all the instrumented services running on your production +environment: `service.environment : production`. Or filter the list by including the APM agent's name and the host it’s running on: +`service.environment : "production" and agent.name : "java" and host.name : "prod-server1"`. + +On the *Traces* view, you might want to view failed transaction results from any of your running containers: +`transaction.result :"FAILURE" and container.id : *`. + +On the *Transactions* view, you may want to list only the slower transactions than a specified time threshold: `transaction.duration.us > 2000000`. +Or filter the list by including the service version and the Kubernetes pod it's running on: +`transaction.duration.us > 2000000 and service.version : "7.12.0" and kubernetes.pod.name : "pod-5468b47f57-pqk2m"`. [float] [[discover-advanced-queries]] === Querying in Discover Alternatively, you can query your APM documents in {kibana-ref}/discover.html[*Discover*]. -Querying documents in *Discover* works the same way as querying in the APM app, +Querying documents in *Discover* works the same way as queries in the APM app, and *Discover* supports all of the example APM app queries shown on this page. [float] -==== Example Discover query +[[discover-queries]] +==== Discover queries One example where you may want to make use of *Discover*, -is for viewing _all_ transactions for an endpoint, instead of just a sample. +is to view _all_ transactions for an endpoint instead of just a sample. TIP: Starting in v7.6, you can view ten samples per bucket in the APM app, instead of just one. diff --git a/docs/apm/correlations.asciidoc b/docs/apm/correlations.asciidoc index 1776cd72ac584..e184ca6bfa656 100644 --- a/docs/apm/correlations.asciidoc +++ b/docs/apm/correlations.asciidoc @@ -3,7 +3,8 @@ === Find latency and error correlations **Correlations** surface attributes of your data that are potentially correlated with high-latency or erroneous transactions. -Surfaced attributes are user-defined, meaning that they are completely customizable to your APM data. +By default, a number of attributes commonly known to cause performance issues, like version, +infrastructure, and location, are included, but all are completely customizable to your APM data. Find something interesting? A quick click of a button will auto-query your data as you work to resolve the underlying issue. For example, a site reliability engineer, who is responsible for keeping production systems up and running, @@ -11,8 +12,7 @@ notices an increase in latency in certain transactions. Analyzing metadata or tags that exist in high-latency transactions but not in lower-latency transactions can potentially point towards the root cause. They may find that a particular piece of hardware, like a host or pod, has failed, increasing latency. -Or, perhaps a set of users, based on IP address or region, is physically too far away from the nearest -data center, increasing latency. +Or, perhaps set of users, based on IP address or region, is facing increased latency due to local data center issues. [discrete] [[view-correlations]] @@ -27,8 +27,8 @@ Queries within the APM app apply to the correlations shown in the correlations f If a correlated field seems noteworthy, use the **Filter** quick links: -* `+` creates a new query in the APM app for transactions containing the selected value. -* `-` creates a new query in the APM app for transactions without the selected value. +* `+` creates a new query in the APM app for filtering transactions containing the selected value. +* `-` creates a new query in the APM app to filter out transactions containing the selected value. [discrete] [[correlations-latency]] @@ -37,8 +37,9 @@ If a correlated field seems noteworthy, use the **Filter** quick links: Correlations help you discover which fields are contributing to increased service latency. A latency distribution chart visualizes the overall latency of the selected service's transactions. -Correlated attributes are sorted by _Impact_–a visual representation of the score for the underlying -aggregation that powers correlations. +Correlated attributes are sorted by _Impact_–a visual representation of the +{ref}/search-aggregations-bucket-significantterms-aggregation.html[significant terms aggregation] +score that powers correlations. Attributes with a high impact, or attributes present in a large percentage of slow transactions, may contribute to increased latency. @@ -51,10 +52,15 @@ exists primarily in higher-latency transactions between 3.7 and 8.7 seconds. [role="screenshot"] image::apm/images/correlations-hover.png[Correlations hover effect] -Selecting the `+` filter creates a new query in the APM app for transactions with +Select the `+` filter to create a new query in the APM app for transactions with `user_agent.name: HeadlessChrome`. With the "noise" now filtered out, you can begin viewing sample traces to continue your investigation. +As you sift through high-latency transactions, you'll likely notice other interesting attributes. +Return to the correlations fly-out and select *Customize fields* to search on these new attributes. +You may need to do this a few times–each time filtering out more and more noise and bringing you +closer to a diagnosis. + [discrete] [[correlations-error-rate]] ==== Find error rate correlations @@ -62,8 +68,9 @@ you can begin viewing sample traces to continue your investigation. Correlations help you discover which fields are contributing to failed transactions. The Error rate over time chart visualizes the change in error rate over the selected time frame. -Correlated attributes are sorted by _Impact_–a visual representation of the score for the underlying -aggregation that powers correlations. +Correlated attributes are sorted by _Impact_–a visual representation of the +{ref}/search-aggregations-bucket-significantterms-aggregation.html[significant terms aggregation] +score that powers correlations. Attributes with a high impact, or attributes present in a large percentage of failed transactions, may contribute to increased error rates. @@ -76,16 +83,41 @@ existed in 100% of failed transactions between 6:00 and 10:30. [role="screenshot"] image::apm/images/error-rate-hover.png[Correlations errors hover effect] -Selecting the `+` filter creates a new query in the APM app for transactions with +Select the `+` filter to create a new query in the APM app for transactions with `url.original: http://localhost:3100...`. With the "noise" now filtered out, you can begin viewing sample traces to continue your investigation. +As you sift through erroneous transactions, you'll likely notice other interesting attributes. +Return to the correlations fly-out and select *Customize fields* to search on these new attributes. +You may need to do this a few times–each time filtering out more and more noise and bringing you +closer to a diagnosis. + [discrete] -[[correlations-custom-fields]] +[[correlations-customize-fields]] ==== Customize fields Correlations are only as good as the data they're searching for. -By default, a handful of potentially useful fields are selected, like `lables`, `service.version`, and `host.ip`. -You can remove and add fields to this list under the **Customize fields** dropdown. +By default, a handful of attributes commonly known to cause performance issues are included. +During the course of an investigation however, you may to need to add and remove fields from +this list multiple times as you narrow in on a diagnosis. + +Add and remove fields under the **Customize fields** dropdown. +The following fields are selected by default. +To keep the default list manageable, only the first six matching fields with wildcards are used. + +**Frontend (RUM) agent:** + +* `labels.*` +* `user.*` +* `user_agent.name` +* `user_agent.os.name` +* `url.original` + +**Backend agents:** + +* `labels.*` +* `host.ip` +* `service.node.name` +* `service.version` TIP: Want to start over? Select **reset** to clear your customizations. diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index 3fe9146658eef..56602ab7c05c9 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -6,49 +6,34 @@ Filter data ++++ -APM provides two different ways you can filter your data within the APM App: - -* <> -* <> - -[[global-filters]] -==== Global filters - -Global filters are ways you can filter any and all data across the APM app. -They are available in the Services, Transactions, Errors, Metrics, and Traces views, -and any filter applied will persist as you move between pages. +Global filters are ways you can filter data across the APM app based on a specific +time range or environment. They are available in the Services, Transactions, Errors, +Metrics, and Traces views, and any filter applied will persist as you move between pages. [role="screenshot"] image::apm/images/global-filters.png[Global filters available in the APM app in Kibana] -[float] -===== Global time range - -The <> in {kib} restricts APM data to a specific time period. - -[float] -[[query-bar]] -===== Query bar +[NOTE] +===== +If you prefer to use advanced queries on your data to filter on specific pieces +of information, see <>. +===== -The query bar is a powerful data query feature. -Similar to the query bar in {kibana-ref}/discover.html[Discover], -it enables you to pass advanced queries on your data to filter on particular pieces of information that you're interested in. -It comes with a handy autocomplete that helps find the fields and even provides suggestions to the data they include. -You can select the query bar and hit the down arrow on your keyboard to begin seeing recommendations. +[[global-time-range]] +==== Global time range -See <> for more information and sample queries. +The <> in {kib} restricts APM data to a specific time period. -[float] [[environment-selector]] -===== Service environment filter +==== Service environment filter The environment selector is a global filter for `service.environment`. -It allows you to view only relevant data, and is especially useful for separating development from production environments. +It allows you to view only relevant data and is especially useful for separating development from production environments. By default, all environments are displayed. If there are no environment options, you'll see "not defined". Service environments are defined when configuring your APM agents. It's vital to be consistent when naming environments in your agents. -See the documentation for each agent you're using to learn how to configure service environments: +To learn how to configure service environments, see the specific agent documentation: * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] * *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] @@ -58,19 +43,3 @@ See the documentation for each agent you're using to learn how to configure serv * *Python:* {apm-py-ref}/configuration.html#config-environment[`environment`] * *Ruby:* {apm-ruby-ref}/configuration.html#config-environment[`environment`] * *Real User Monitoring:* {apm-rum-ref}/configuration.html#environment[`environment`] - -[[contextual-filters]] -==== Contextual filters - -Contextual filters are ways you can filter your specific APM data on each individual page. -The filters shown are relevant to your data, and will persist between pages, -but only where they are applicable -- they are typically most useful in their original context. -As an example, if you select a host on the Services overview, then select a transaction group, -the host filter will still be applied. - -These filters are very useful for quickly and easily removing noise from your data. -With just a click, you can filter your transactions by the transaction result, -host, container ID, Kubernetes pod, and more. - -[role="screenshot"] -image::apm/images/local-filter.png[Local filters available in the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/images/apm-errors-overview.png b/docs/apm/images/apm-errors-overview.png index 5b3b00a3b1ef2..425464a1ffd21 100644 Binary files a/docs/apm/images/apm-errors-overview.png and b/docs/apm/images/apm-errors-overview.png differ diff --git a/docs/apm/images/apm-metrics.png b/docs/apm/images/apm-metrics.png index af083b5ba3c08..c2d609c7c4cd5 100644 Binary files a/docs/apm/images/apm-metrics.png and b/docs/apm/images/apm-metrics.png differ diff --git a/docs/apm/images/apm-query-bar.png b/docs/apm/images/apm-query-bar.png index 92398065c2545..a1fb129d3c200 100644 Binary files a/docs/apm/images/apm-query-bar.png and b/docs/apm/images/apm-query-bar.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 3a56d597abfb7..1c16ac5b572c3 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-traces.png b/docs/apm/images/apm-traces.png index ed15423b42c51..0e9062ee448b4 100644 Binary files a/docs/apm/images/apm-traces.png and b/docs/apm/images/apm-traces.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index 1b25668f0fd92..be292c37e24e0 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/global-filters.png b/docs/apm/images/global-filters.png index 70ae50aea6057..f93a5214c316b 100644 Binary files a/docs/apm/images/global-filters.png and b/docs/apm/images/global-filters.png differ diff --git a/docs/apm/images/jvm-metrics-overview.png b/docs/apm/images/jvm-metrics-overview.png index 4b882574e2b9a..c6f28f7bdf48f 100644 Binary files a/docs/apm/images/jvm-metrics-overview.png and b/docs/apm/images/jvm-metrics-overview.png differ diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 5049321363f88..8cab7bb03da75 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -20,7 +20,7 @@ don't forget to check our other troubleshooting guides or discussion forum: * {apm-php-ref}/troubleshooting.html[PHP agent troubleshooting] * {apm-py-ref}/troubleshooting.html[Python agent troubleshooting] * {apm-ruby-ref}/debugging.html[Ruby agent troubleshooting] -* {apm-rum-ref/troubleshooting.html[RUM troubleshooting] +* {apm-rum-ref}/troubleshooting.html[RUM troubleshooting] * https://discuss.elastic.co/c/apm[APM discussion forum]. [discrete] diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 54c065480b113..88a6a6a0af8ae 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -101,6 +101,7 @@ readonly links: { readonly indexPatterns: { readonly loadingData: string; readonly introduction: string; + readonly fieldFormattersNumber: string; }; readonly addData: string; readonly kibana: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 0bca16a0bb710..41ce5e9ab1fab 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
readonly fieldFormattersNumber: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md index e4ac35f19e959..93dfdeb056f15 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md @@ -7,5 +7,5 @@ Signature: ```typescript -isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined +isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md new file mode 100644 index 0000000000000..5188a951c149f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [submitOnBlur](./kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md) + +## QueryStringInputProps.submitOnBlur property + +Signature: + +```typescript +submitOnBlur?: boolean; +``` diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc deleted file mode 100644 index d9745bfef524a..0000000000000 --- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc +++ /dev/null @@ -1,170 +0,0 @@ -[role="xpack"] -[[ingest-node-pipelines]] -== Ingest Node Pipelines - -*Ingest Node Pipelines* enables you to create and manage {es} -pipelines that perform common transformations and -enrichments on your data. For example, you might remove a field, -rename an existing field, or set a new field. - -To begin, open the main menu, then click *Stack Management > Ingest Node Pipelines*. With *Ingest Node Pipelines*, you can: - -* View a list of your pipelines and drill down into details. -* Create a pipeline that defines a series of tasks, known as processors. -* Test a pipeline before feeding it with real data to ensure the pipeline works as expected. -* Delete a pipeline that is no longer needed. - -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-list.png["Ingest node pipeline list"] - -[float] -=== Required permissions - -The minimum required permissions to access *Ingest Node Pipelines* are -the `manage_pipeline` and `cluster:monitor/nodes/info` cluster privileges. - -To add privileges, open the main menu, then click *Stack Management > Roles*. - -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-privileges.png["Privileges required for Ingest Node Pipelines"] - -[float] -[[ingest-node-pipelines-manage]] -=== Manage pipelines - -From the list view, you can to drill down into the details of a pipeline. -To -edit, clone, or delete a pipeline, use the *Actions* menu. - -If you don’t have any pipelines, you can create one using the -*Create pipeline* form. You’ll define processors to transform documents -in a specific way. To handle exceptions, you can optionally define -failure processors to execute immediately after a failed processor. -Before creating the pipeline, you can verify it provides the expected output. - -[float] -[[ingest-node-pipelines-example]] -==== Example: Create a pipeline - -In this example, you’ll create a pipeline to handle server logs in the -Common Log Format. The log looks similar to this: - -[source,js] ----------------------------------- -212.87.37.154 - - [05/May/2020:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" -200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) -AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\" ----------------------------------- - -The log contains an IP address, timestamp, and user agent. You want to give -these three items their own field in {es} for fast search and visualization. -You also want to know where the request is coming from. - -. In *Ingest Node Pipelines*, click *Create a pipeline*. -. Provide a name and description for the pipeline. -. Add a grok processor to parse the log message: - -.. Click *Add a processor* and select the *Grok* processor type. -.. Set the field input to `message` and enter the following grok pattern: -+ -[source,js] ----------------------------------- -%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:response:int} (?:-|%{NUMBER:bytes:int}) %{QS:referrer} %{QS:agent} ----------------------------------- -+ -.. Click *Update* to save the processor. - -. Add processors to map the date, IP, and user agent fields. - -.. Map the appropriate field to each processor type: -+ --- -* **Date**: `timestamp` -* **GeoIP**: `clientip` -* **User agent**: `agent` - -For the **Date** processor, you also need to specify the date format you want to use: `dd/MMM/YYYY:HH:mm:ss Z`. --- -Your form should look similar to this: -+ -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] -+ -Alternatively, you can click the **Import processors** link and define the processors as JSON: -+ -[source,js] ----------------------------------- -{ - "processors": [ - { - "grok": { - "field": "message", - "patterns": ["%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \\[%{HTTPDATE:timestamp}\\] \"%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:httpversion}\" %{NUMBER:response:int} (?:-|%{NUMBER:bytes:int}) %{QS:referrer} %{QS:agent}"] - } - }, - { - "date": { - "field": "timestamp", - "formats": [ "dd/MMM/YYYY:HH:mm:ss Z" ] - } - }, - { - "geoip": { - "field": "clientip" - } - }, - { - "user_agent": { - "field": "agent" - } - } - ] -} ----------------------------------- -+ -The four {ref}/ingest-processors.html[processors] will run sequentially: -{ref}/grok-processor.html[grok], {ref}/date-processor.html[date], -{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. You can reorder processors using the arrow icon next to each processor. - -. To test the pipeline to verify that it produces the expected results, click *Add documents*. - -. In the *Documents* tab, provide a sample document for testing: -+ -[source,js] ----------------------------------- -[ - { - "_source": { - "message": "212.87.37.154 - - [05/May/2020:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" 200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\"" - } - } -] ----------------------------------- - -. Click *Run the pipeline* and check if the pipeline worked as expected. -+ -You can also -view the verbose output and refresh the output from this view. - -. If everything looks correct, close the panel, and then click *Create pipeline*. -+ -At this point, you’re ready to use the Elasticsearch index API to load -the logs data. - -. In the Kibana Console, index a document with the pipeline -you created. -+ -[source,js] ----------------------------------- -PUT my-index/_doc/1?pipeline=access_logs -{ - "message": "212.87.37.154 - - [05/May/2020:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" 200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\"" -} ----------------------------------- - -. To verify, run: -+ -[source,js] ----------------------------------- -GET my-index/_doc/1 ----------------------------------- diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 79ce9fdbd1a7e..893873eb1075a 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -10,8 +10,8 @@ Numeral formatting patterns are used in multiple places in {kib}, including: * <> * <> -* <> -* <> +* <> +* <> The simplest pattern format is `0`, and the default {kib} pattern is `0,0.[000]`. The numeral pattern syntax expresses: diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index fa3e13de65153..538f963c0bd00 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -179,11 +179,6 @@ This content has moved. See <>. This content has moved. See <>. -[role="exclude",id="lens"] -== Lens - -This content has moved. See <>. - [role="exclude",id="known-plugins"] == Known plugins @@ -257,7 +252,7 @@ This page has been moved. Refer to <> [[heatmap-chart]] === Heatmap Chart -This page has been moved. Refer to <>. +This page has been moved. Refer to <>. [float] [[interface-overview]] @@ -296,3 +291,14 @@ This content has moved. Refer to <>. [role="exclude",id="explore-dashboard-data"] This content has moved. Refer to <>. + +[role="exclude",id="ingest-node-pipelines"] +== Ingest Node Pipelines + +This content has moved. See {ref}/ingest.html[Ingest pipelines]. + + +[role="exclude",id="create-panels-with-timelion"] +== Timelion + +This content has moved. refer to <>. diff --git a/docs/settings/banners-settings.asciidoc b/docs/settings/banners-settings.asciidoc new file mode 100644 index 0000000000000..2a68cbe82f9f2 --- /dev/null +++ b/docs/settings/banners-settings.asciidoc @@ -0,0 +1,35 @@ +[role="xpack"] +[[banners-settings-kb]] +=== Banner settings in {kib} +++++ +Banners settings +++++ + +Banners are disabled by default. You need to manually configure them in order to use the feature. + +You can configure the `xpack.banners` settings in your `kibana.yml` file. + +[[general-banners-settings-kb]] +==== General banner settings + +[cols="2*<"] +|=== + +| `xpack.banners.placement` +| Set to `header` to enable the header banner. Defaults to `disabled`. + +| `xpack.banners.textContent` +| The text to display inside the banner, either plain text or Markdown. + +| `xpack.banners.textColor` +| The color for the banner text. Defaults to `#8A6A0A`. + +| `xpack.banners.backgroundColor` +| The color of the banner background. Defaults to `#FFF9E8`. + +|=== + +[NOTE] +==== +The `banners` plugin is a https://www.elastic.co/subscriptions[subscription feature] +==== \ No newline at end of file diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index 4a211976be8cf..1bd38578750d7 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -12,6 +12,7 @@ For more {kib} configuration settings, see <>. include::alert-action-settings.asciidoc[] include::apm-settings.asciidoc[] +include::banners-settings.asciidoc[] include::dev-settings.asciidoc[] include::graph-settings.asciidoc[] include::infrastructure-ui-settings.asciidoc[] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 070bf03b39e4b..699a5d28458a6 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -671,6 +671,7 @@ Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] include::{kib-repo-dir}/settings/apm-settings.asciidoc[] +include::{kib-repo-dir}/settings/banners-settings.asciidoc[] include::{kib-repo-dir}/settings/dev-settings.asciidoc[] include::{kib-repo-dir}/settings/graph-settings.asciidoc[] include::{kib-repo-dir}/settings/fleet-settings.asciidoc[] diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index e5ac44a4e5401..1face015f1f76 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -126,7 +126,7 @@ image::images/canvas-element-select.gif[Canvas elements] * *{es} documents* — Access your data in {es} without using aggregations. To use, select an index and fields, and optionally enter a query using the <>. Use the *{es} documents* data source when you have low volume datasets, to view raw documents, or to plot exact, non-aggregated values on a chart. -* *Timelion* — Access your time series data using <> queries. To use *Timelion* queries, you can enter a query using the <>. +* *Timelion* — Access your time series data using <> queries. To use *Timelion* queries, you can enter a query using the <>. + Each element can display a different data source, and pages and workpads often contain multiple data sources. diff --git a/docs/user/dashboard/advanced-editors.asciidoc b/docs/user/dashboard/advanced-editors.asciidoc deleted file mode 100644 index e2e32c8373f5a..0000000000000 --- a/docs/user/dashboard/advanced-editors.asciidoc +++ /dev/null @@ -1,65 +0,0 @@ -[[add-panels-with-advanced-editors]] -== Create panels with the advanced editors - -{kib} provides you with three advanced editors that you can use to manually create dashboard panels. - -[float] -[[tsvb-advanced-editor]] -=== TSVB - -*TSVB* is a time series data visualization editor that allows you to use the full power of the {es} aggregation framework. - -With *TSVB*, you can: - -* Combine an infinite number of <> to display your data. -* Annotate time series data with timestamped events from an {es} index. -* View the data in several types of visualizations, including charts, data tables, and markdown panels. -* Display multiple <> in each visualization. -* Customize the data with labels and colors. - -image::images/tsvb.png[TSVB UI] - -[float] -[[vega-advanced-editor]] -=== Custom visualizations - -*Vega* and *Vega-Lite* are visualization grammars that are integrated into {kib} for custom visualizations. - -* *Vega-Lite* — A high-level grammar for rapid analysis - -* *Vega* — A declarative language with support for interactivity - -*Vega* and *Vega-Lite* panels can display one or more data sources, including {es}, Elastic Map Service, -URL, or static data, and support <> that allow you to embed the panels on your dashboard and add interactive tools. - -Use *Vega* or *Vega-Lite* when you want to create visualizations with: - -* Aggregations that use `nested` or `parent/child` mapping -* Aggregations without an index pattern -* Queries that use custom time filters -* Complex calculations -* Extracted data from _source instead of aggregations -* Scatter charts, sankey charts, and custom maps -* An unsupported visual theme - -These grammars have some limitations: they do not support tables, and can't run queries conditionally. - -image::images/vega.png[Vega UI] - -For detailed *Vega* and *Vega-Lite* information and examples, refer to <>. - -[float] -[[timelion-advanced-editor]] -=== Timelion - -*Timelion* is driven by a simple expression language that you use to: - -* Retrieve time series data from one or more indices -* Perform math across two or more time series -* Visualize the results - -image:dashboard/images/timelion.png[Timelion] - -include::tsvb.asciidoc[] -include::vega.asciidoc[] -include::timelion.asciidoc[] \ No newline at end of file diff --git a/docs/user/dashboard/aggregation-based.asciidoc b/docs/user/dashboard/aggregation-based.asciidoc index b086214c87ed4..49c092f8baa4c 100644 --- a/docs/user/dashboard/aggregation-based.asciidoc +++ b/docs/user/dashboard/aggregation-based.asciidoc @@ -1,32 +1,14 @@ [[add-aggregation-based-visualization-panels]] -== Create aggregation-based visualization panels +=== Aggregation-based Aggregation-based visualizations are the core {kib} panels, and are not optimized -for a specific use case. If you are new to {kib}, <> is recommended as a -simpler way to get started. - -The main features that these panel types have compared to other {kib} panel types are: - -* Support for heat map, tag cloud, gauge, and goal chart types -* Support for split charts at up to 3 levels of aggregation, more than *Lens* and *TSVB* -* Time series data is not required -* Ability to use a <> as an input -* Data table can be sorted, also supports summary row and percentage column features -* Can assign specific colors to series -* Ability for plugin authors to extend the features - -These panel types also have some limitations: - -* Not as simple as *Lens* -* Limited styling options -* No support for math -* No support for multiple indices +for a specific use case. [float] -[[types-of-visualization-panels]] -=== Types of aggregation-based panels +[[types-of-visualizations]] +==== Types of aggregation-based visualizations -{kib} supports the following types of aggregation-based panels. +{kib} supports the following types of aggregation-based visualizations. [cols="50, 50"] |=== @@ -106,13 +88,13 @@ create visual art for a specific topic. [float] [[create-aggregation-based-panel]] -=== Create an aggregation-based panel +==== Create an aggregation-based visualization panel -Choose the type of panel you want to create, then use the editor to configure the options. Each panel type supports different options. +Choose the type of visualization you want to create, then use the editor to configure the options. . From the dashboard, click *Create panel*, then click *Aggregation based* on the *New visualization* window. -.. Click the type of panel you want to create. +.. Click the type of visualization you want to create. .. Click the data source you want to visualize. @@ -132,13 +114,13 @@ image:images/aggregation-based-color-picker.png[Color picker] [float] [[try-it-aggregation-based-panel]] -=== Try it: Create an aggregation-based bar chart +==== Try it: Create an aggregation-based bar chart You collected data from your web server, and you want to visualize and analyze the data on a dashboard. To create a dashboard panel of the data, create a bar chart that displays the top five log traffic sources for every three hours. [float] -==== Add the data and create the dashboard +===== Add the data and create the dashboard Add the sample web logs data that you'll use to create the bar chart, then create the dashboard. @@ -151,7 +133,7 @@ Add the sample web logs data that you'll use to create the bar chart, then creat . On the *Dashboards* page, click *Create dashboard*. [float] -==== Open and set up the aggregation-based bar chart +===== Open and set up the aggregation-based bar chart Open the bar chart visualization builder and change the time range. @@ -165,7 +147,7 @@ Open the bar chart visualization builder and change the time range. [float] [[tutorial-configure-the-bar-chart]] -==== Create the bar chart +===== Create the bar chart To create the bar chart, add a <>, then add the terms sub-aggregation to display the top values. @@ -196,7 +178,7 @@ TIP: Aggregation-based panels support a maximum of three *Split series*. image:images/bar-chart-tutorial-2.png[Bar chart with sample logs data] [float] -==== Save the panel +===== Save the panel Save and add the visualization panel to the dashboard. diff --git a/docs/user/dashboard/create-panels-with-editors.asciidoc b/docs/user/dashboard/create-panels-with-editors.asciidoc new file mode 100644 index 0000000000000..8e047819fd1c6 --- /dev/null +++ b/docs/user/dashboard/create-panels-with-editors.asciidoc @@ -0,0 +1,131 @@ +[[create-panels-with-editors]] +== Create panels with editors + +{kib} provides several editors that you can use to create dashboard panels. + +[cols="2"] +|=== + +| <> +| Create visualizations with the drag and drop editor. *Lens* is recommended for most users. + +| <> +| Create visualizations with your geographical data. + +| <> +| Create visualizations with your time series data. + +| <> +| Create custom visualizations with the *Vega* and *Vega-Lite* grammars. + +| <> +| Build most visualization types using {es} <>. + +| <> +| Create visualizations with your time series data using a simple expression language. + +|=== + +[float] +[[lens-editor]] +=== Lens + +*Lens* is the drag and drop editor that creates visualizations of your data. + +With *Lens*, you can: + +* Use the automatically generated suggestions to change the visualization type. +* Create visualizations with multiple layers and indices. +* Change the aggregation and labels to customize the data. + +[role="screenshot"] +image:dashboard/images/lens_advanced_1_1.png[Lens] + +[float] +[[tsvb-editor]] +=== TSVB + +*TSVB* is a time series data visualization editor that allows you to use the full power of the {es} aggregation framework. + +With *TSVB*, you can: + +* Combine an infinite number of <> to display your data. +* Annotate time series data with timestamped events from an {es} index. +* View the data in several types of visualizations, including charts, data tables, and markdown panels. +* Display multiple <> in each visualization. +* Customize the data with labels and colors. + +[role="screenshot"] +image::images/tsvb.png[TSVB UI] + +[float] +[[custom-visualizations]] +=== Custom visualizations + +*Vega* and *Vega-Lite* are visualization grammars that are integrated into {kib} for custom visualizations. + +* *Vega-Lite* — A high-level grammar for rapid analysis. + +* *Vega* — A declarative language with support for interactivity. + +*Vega* and *Vega-Lite* panels can display one or more data sources, including {es}, Elastic Map Service, +URL, or static data, and support <> that allow you to embed the panels on your dashboard and add interactive tools. + +Use *Vega* or *Vega-Lite* when you want to create visualizations with: + +* Aggregations that use `nested` or `parent/child` mapping +* Aggregations without an index pattern +* Queries that use custom time filters +* Complex calculations +* Extracted data from _source instead of aggregations +* Scatter charts, sankey charts, and custom maps +* An unsupported visual theme + +These grammars have some limitations: they do not support tables, and can't run queries conditionally. + +[role="screenshot"] +image::images/vega.png[Vega UI] + +For detailed *Vega* and *Vega-Lite* information and examples, refer to <>. + +[float] +[[aggregation-based]] +=== Aggregation-based + +With aggregation-based visualizations, you can: + +* Create heat map, tag cloud, gauge, and goal visualizations +* Split charts up to three aggregation levels, which is more than *Lens* and *TSVB* +* Time series data is not required +* Use a <> as an input +* Sort data tables and use the summary row and percentage column features +* Assign colors to series +* Extend features with plugins + +[role="screenshot"] +image:dashboard/images/aggregation_based.png[Aggregation-based editor] + +Aggregation-based visualizations include the following limitations: + +* Limited styling options +* Math is unsupported +* Multiple indices is unsupported + +[float] +[[timelion-editor]] +=== Timelion + +*Timelion* is driven by a simple expression language that you use to: + +* Retrieve time series data from one or more indices +* Perform math across two or more time series +* Visualize the results + +[role="screenshot"] +image:dashboard/images/timelion.png[Timelion] + +include::lens.asciidoc[] +include::tsvb.asciidoc[] +include::vega.asciidoc[] +include::aggregation-based.asciidoc[] +include::timelion.asciidoc[] \ No newline at end of file diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 1752f067801b4..89fa564b0ac71 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -12,25 +12,18 @@ then compare the panels side-by-side to identify the patterns and connections in [role="screenshot"] image:images/Dashboard_example.png[Example dashboard] -[[tsvb]] - Dashboards support many types of panels, and provide several editors that you can use to create panels. [cols="2"] |=== -| <> -| The drag and drop editor that creates visualizations of your data. *Lens* is recommended for most users. +| <> +| Use the *Lens*, *TSVB*, *Vega*, and *Timelion* editors to help you create visualizations of your data, or create aggregation-based visualizations using {es} <>. +*Lens* is recommended for most users. | <> | Create beautiful displays of your geographical data. -| <> -| Visualize time series data with *TSVB* or *Timelion*, or create a custom visualization using *Vega* or *Vega-Lite*. - -| <> -| Build most visualization types using {es} <>. - | <> | Add context to your panels with <>, or add dynamic filters with <>. @@ -152,6 +145,8 @@ Add a panel that displays the results from machine learning anomaly detection jo . On the *Add from library* flyout, click *Create new*, then select *ML Anomaly Swim Lane*. +[[tsvb]] + [float] [[arrange-panels]] [[moving-containers]] @@ -332,11 +327,16 @@ for *Lens* panels. [role="xpack"] To download *Lens* panel data in a CSV file: -Open the *Lens* panel menu, then select *More > Download as CSV*. +. Open the *Lens* panel menu. +. Select *More > Download as CSV*. ++ [role="screenshot"] image::images/download_csv_context_menu.png[Download as CSV from panel context menu] +Each layer produces a single CSV file with columns. +When you download multiple layers, the file names combine the visualization and layer index names. + To download all other panel data in a CSV file: . Open the panel menu, then select *Inspect*. @@ -385,11 +385,11 @@ log in using their {kib} credentials, via reverse proxy, or enable <>. It is important to export dashboards with all references needed. -- -include::lens.asciidoc[] +include::tutorial-create-a-dashboard-of-lens-panels.asciidoc[] -include::advanced-editors.asciidoc[] +include::lens-advanced.asciidoc[] -include::aggregation-based.asciidoc[] +include::create-panels-with-editors.asciidoc[] include::enhance-dashboards.asciidoc[] diff --git a/docs/user/dashboard/images/aggregation_based.png b/docs/user/dashboard/images/aggregation_based.png new file mode 100644 index 0000000000000..0e34dbf665811 Binary files /dev/null and b/docs/user/dashboard/images/aggregation_based.png differ diff --git a/docs/user/dashboard/images/lens.png b/docs/user/dashboard/images/lens.png new file mode 100644 index 0000000000000..a78af1414df73 Binary files /dev/null and b/docs/user/dashboard/images/lens.png differ diff --git a/docs/user/dashboard/images/lens_advanced_1_1.png b/docs/user/dashboard/images/lens_advanced_1_1.png new file mode 100644 index 0000000000000..9d67f5cc691ff Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_1_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_1_1_2.png b/docs/user/dashboard/images/lens_advanced_1_1_2.png new file mode 100644 index 0000000000000..8b5fe130ce7b7 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_1_1_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_1_2.png b/docs/user/dashboard/images/lens_advanced_1_2.png new file mode 100644 index 0000000000000..bc5d1f67be46c Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_1_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_1.png b/docs/user/dashboard/images/lens_advanced_2_1.png new file mode 100644 index 0000000000000..5090f0d3b2841 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_1_1.png b/docs/user/dashboard/images/lens_advanced_2_1_1.png new file mode 100644 index 0000000000000..f4d9ca488782e Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_2_1_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_2.png b/docs/user/dashboard/images/lens_advanced_2_2.png new file mode 100644 index 0000000000000..820bc3bd4dfa9 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_2_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_2_1.png b/docs/user/dashboard/images/lens_advanced_2_2_1.png new file mode 100644 index 0000000000000..3124dd1de0654 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_2_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_1.gif b/docs/user/dashboard/images/lens_advanced_3_1.gif new file mode 100644 index 0000000000000..5fdf58eb2fc86 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_3_1.gif differ diff --git a/docs/user/dashboard/images/lens_advanced_3_1_1.png b/docs/user/dashboard/images/lens_advanced_3_1_1.png new file mode 100644 index 0000000000000..4d52a23cc2cff Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_3_1_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_2.png b/docs/user/dashboard/images/lens_advanced_3_2.png new file mode 100644 index 0000000000000..20da2ed706dfd Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_3_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_3.png b/docs/user/dashboard/images/lens_advanced_3_3.png new file mode 100644 index 0000000000000..1d88bcd238ca3 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_3_3.png differ diff --git a/docs/user/dashboard/images/lens_advanced_4_1.png b/docs/user/dashboard/images/lens_advanced_4_1.png new file mode 100644 index 0000000000000..43c8db213d482 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_4_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_4_2.png b/docs/user/dashboard/images/lens_advanced_4_2.png new file mode 100644 index 0000000000000..4b3e98910e7b7 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_4_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_result.png b/docs/user/dashboard/images/lens_advanced_result.png new file mode 100644 index 0000000000000..19963d87c8e1c Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_result.png differ diff --git a/docs/user/dashboard/images/lens_area_percentage.png b/docs/user/dashboard/images/lens_area_percentage.png new file mode 100644 index 0000000000000..e682dbb24c056 Binary files /dev/null and b/docs/user/dashboard/images/lens_area_percentage.png differ diff --git a/docs/user/dashboard/images/lens_bucketed_aggregation_advanced_dropdown.png b/docs/user/dashboard/images/lens_bucketed_aggregation_advanced_dropdown.png new file mode 100644 index 0000000000000..f4616905ad344 Binary files /dev/null and b/docs/user/dashboard/images/lens_bucketed_aggregation_advanced_dropdown.png differ diff --git a/docs/user/dashboard/images/lens_drag_drop.gif b/docs/user/dashboard/images/lens_drag_drop.gif deleted file mode 100644 index 22939467daa12..0000000000000 Binary files a/docs/user/dashboard/images/lens_drag_drop.gif and /dev/null differ diff --git a/docs/user/dashboard/images/lens_drag_drop_1.gif b/docs/user/dashboard/images/lens_drag_drop_1.gif new file mode 100644 index 0000000000000..e2fc30fc7caed Binary files /dev/null and b/docs/user/dashboard/images/lens_drag_drop_1.gif differ diff --git a/docs/user/dashboard/images/lens_drag_drop_1.png b/docs/user/dashboard/images/lens_drag_drop_1.png new file mode 100644 index 0000000000000..3462e26026d25 Binary files /dev/null and b/docs/user/dashboard/images/lens_drag_drop_1.png differ diff --git a/docs/user/dashboard/images/lens_drag_drop_2.png b/docs/user/dashboard/images/lens_drag_drop_2.png new file mode 100644 index 0000000000000..4c3c35ce58c9f Binary files /dev/null and b/docs/user/dashboard/images/lens_drag_drop_2.png differ diff --git a/docs/user/dashboard/images/lens_drag_drop_3.gif b/docs/user/dashboard/images/lens_drag_drop_3.gif new file mode 100644 index 0000000000000..39211300301b0 Binary files /dev/null and b/docs/user/dashboard/images/lens_drag_drop_3.gif differ diff --git a/docs/user/dashboard/images/lens_fields_indexpattern.png b/docs/user/dashboard/images/lens_fields_indexpattern.png new file mode 100644 index 0000000000000..87804fd06020d Binary files /dev/null and b/docs/user/dashboard/images/lens_fields_indexpattern.png differ diff --git a/docs/user/dashboard/images/lens_ip_mixed_sorting.png b/docs/user/dashboard/images/lens_ip_mixed_sorting.png new file mode 100644 index 0000000000000..b10d77b723583 Binary files /dev/null and b/docs/user/dashboard/images/lens_ip_mixed_sorting.png differ diff --git a/docs/user/dashboard/images/lens_ipv4_sorting.png b/docs/user/dashboard/images/lens_ipv4_sorting.png new file mode 100644 index 0000000000000..cec8c458d1f3c Binary files /dev/null and b/docs/user/dashboard/images/lens_ipv4_sorting.png differ diff --git a/docs/user/dashboard/images/lens_value_labels_partition_toggle.png b/docs/user/dashboard/images/lens_value_labels_partition_toggle.png new file mode 100644 index 0000000000000..82ee7b1b7eedb Binary files /dev/null and b/docs/user/dashboard/images/lens_value_labels_partition_toggle.png differ diff --git a/docs/user/dashboard/images/lens_value_labels_xychart_toggle.png b/docs/user/dashboard/images/lens_value_labels_xychart_toggle.png new file mode 100644 index 0000000000000..8cf5feb5712ac Binary files /dev/null and b/docs/user/dashboard/images/lens_value_labels_xychart_toggle.png differ diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc new file mode 100644 index 0000000000000..6b090f6017f5d --- /dev/null +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -0,0 +1,259 @@ +[[create-a-dashboard-of-panels-with-ecommerce-data]] +== Tutorial: Create a dashboard of panels with ecommerce sales data + +You collected sales data from your store, and you want to visualize and analyze the data on a dashboard. +To create dashboard panels of the data, open the *Lens* visualization builder, then +create the visualization panels that best display the data. + +When you've completed the tutorial, you'll have a dashboard that provides you with a complete overview of your ecommerce sales data. + +[role="screenshot"] +image::images/lens_advanced_result.png[Dashboard view] + +[discrete] +[[add-the-data-and-create-the-dashboard-advanced]] +=== Add the data and create the dashboard + +To create visualizations of the data from your store, add the data set, then create the dashboard. + +. From the {kib} *Home* page, click *Try our sample data*. + +. From *Sample eCommerce orders*, click *Add data*. + +. Open the main menu, then click *Dashboard*. + +. On the *Dashboards* page, click *Create dashboard*. + +[float] +[[open-and-set-up-lens-advanced]] +=== Open and set up Lens + +Open the *Lens* editor, then make sure the correct fields appear. + +. From the dashboard, click *Create panel*. + +. On the *New visualization* window, click *Lens*. ++ +[role="screenshot"] +image::images/lens_end_to_end_1_1.png[New visualization popover] + +. Make sure the *kibana_sample_data_ecommerce_* index appears. + +[discrete] +[[view-the-number-of-transactions-per-day]] +=== View the number of transactions per hour + +To determine the number of orders made every hour, create a bar chart, then add the chart to the dashboard. + +. Set the <> to *Last 30 days*. + +. From the *Available fields* list, drag and drop *Records* to the visualization builder. ++ +[role="screenshot"] +image::images/lens_advanced_1_1.png[Added records to the workspace] + +. Change the *Vertical axis* title and display the number of orders per day. + +.. In the editor, click *Count of Records*. + +.. In the *Display name* field, enter `Number of orders`. + +.. Click *Add advanced options > Normalize by unit*. + +.. From the *Normalize by unit* dropdown, select *per hour*, then click *Close*. + +. To hide the *Horizontal axis* label, open the *Bottom Axis* menu, then deselect *Show*. ++ +[role="screenshot"] +image::images/lens_advanced_1_1_2.png[Bottom axis menu] ++ +You have a bar chart that shows you how many orders were made at your store every hour. ++ +[role="screenshot"] +image::images/lens_advanced_1_2.png[Orders per day] + +. Click *Save and return*. + +[discrete] +[[view-the-cumulative-number-of-products-sold-over-time]] +=== View the cumulative number of products sold on weekends + +To determine the number of orders made only on Saturday and Sunday, create an area chart, then add it to the dashboard. + +. Open *Lens*. + +. From the *Chart Type* dropdown, select *Area*. ++ +[role="screenshot"] +image::images/lens_advanced_2_1_1.png[Chart type menu with Area selected] + +. Configure the cumulative sum of the store orders. + +.. From the *Available fields* list, drag and drop *Records* to the visualization builder. + +.. From the editor, click *Count of Records*. + +.. From *Select a function*, click *Cumulative sum*. + +.. In the *Display name* field, enter `Cumulative orders during weekend days`, then click *Close*. + +. Filter the results to display the data for only Saturday and Sunday. + +.. From the editor, click the *Drop a field or click to add* field for *Break down by*. + +.. From *Select a function*, click *Filters*. + +.. Click *All records*. + +.. In the *KQL* field, enter `day_of_week : "Saturday" or day_of_week : "Sunday"`, then press Return. ++ +The <> displays all documents where `day_of_week` matches `Saturday` or `Sunday`. ++ +[role="screenshot"] +image::images/lens_advanced_2_1.png[Filter aggregation to filter weekend days] + +. To hide the legend, open the *Legend* menu, then click *Hide*. ++ +[role="screenshot"] +image::images/lens_advanced_2_2_1.png[Legend menu] ++ +You have an area chart that shows you how many orders your store received during the weekend. ++ +[role="screenshot"] +image::images/lens_advanced_2_2.png[Line chart with cumulative sum of orders made on the weekend] + +. Click *Save and return*. + +[discrete] +[[add-a-data-layer-advanced]] +=== Create multiple key percentiles of product prices + +To view the price distribution of products sold over time, create a percentile chart, then add it to the dashboard. + +. Open *Lens*. + +. From the *Chart Type* dropdown, select *Line*. + +. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. + +* Drag and drop *products.price* to the *Vertical axis* field. + +* Drag and drop *order_date* to the *Horizontal axis* field. + +. Create the 95th percentile. + +.. In the editor, click *Median of products.price*. + +.. From *Select a function*, click *Percentile*. + +.. In the *Display name* field, enter `95th`, then click *Close*. + +. To create the 90th percentile, duplicate the `95th` percentile. + +.. Drag and drop *95th* to *Drop a field or click to add*. + +.. Click *95th [1]*, then enter `90` in the *Percentile* field. + +.. In the *Display name* field enter `90th`, then click *Close*. ++ +[role="screenshot"] +image::images/lens_advanced_3_1.gif[Easily duplicate the items with drag and drop] + +. Create the 50th percentile. + +.. Drag and drop *90th* to *Drop a field or click to add*. + +.. Click *90th [1]*, then enter `50` in the *Percentile* field. + +.. In the *Display name* field enter `50th`, then click *Close*. + +. Create the 10th percentile. + +.. Drag and drop *50th* to *Drop a field or click to add*. + +.. Click *50th [1]*, then enter `10` in the *Percentile* field. + +.. In the *Display name* field enter `10th`, then click *Close*. + +. To change the left axis label, open the *Left Axis* menu, then enter `Percentiles for product prices` in the *Axis name* field. ++ +[role="screenshot"] +image::images/lens_advanced_3_1_1.png[Left Axis menu] ++ +You have a line chart that shows you the price distribution of products sold over time. ++ +[role="screenshot"] +image::images/lens_advanced_3_3.png[Percentiles for product prices chart] + +. Click *Save and return*. + +[discrete] +[[add-the-response-code-filters-advanced]] +=== View the moving average of inventory prices + +To view and analyze the prices of shoes, accessories, and clothing in the store inventory, create a line chart. + +. Open *Lens*. + +. From the *Chart Type* dropdown, select *Line*. + +. From the *Available fields* list, drag and drop *products.price* to the visualization builder. + +. In the editor, click the *Drop a field or click to add* field for *Break down by*. + +. From *Select a function*, click *Filters*. + +. Add a filter for shoes. + +.. Click *All records*. + +.. In the *KQL* field, enter `category.keyword : *Shoes*`. + +.. In the *Label* field, enter `Shoes`, then press Return. + +. Add a filter for accessories. + +.. Click *Add a filter*. + +.. In the *KQL* field, enter `category.keyword : *Accessories*`. + +.. In the *Label* field, enter `Accessories`, then press Return. + +. Add a filter for clothing. + +.. Click *Add a filter*. + +.. In the *KQL* field, enter `category.keyword : *Clothing*`. + +.. In the *Label* field, enter `Clothing`, then press Return. + +. Click *Close* ++ +[role="screenshot"] +image::images/lens_advanced_4_1.png[Median prices chart for different categories] + +[discrete] +[[add-the-moving-average]] +==== Add the moving average + +To focus on the general trends rather than on the peaks in the data, add the moving average, then add the visualization to the dashboard. + +. In the editor, click the *Median of products.price*. + +. From *Select a function*, click *Moving average*. + +. In the *Window size* field, enter `7`, then click *Close*. ++ +[role="screenshot"] +image::images/lens_advanced_4_2.png[Moving average prices chart for different categories] + +. Click *Save and return*. + +[discrete] +=== Save the dashboard + +Now that you have a complete overview of your ecommerce sales data, save the dashboard. + +. In the toolbar, click *Save*. + +. On the *Save dashboard* window, enter `Ecommerce sales data`, then click *Save*. diff --git a/docs/user/dashboard/lens-end-to-end/images/non-edited/lens_end_to_end_1.png b/docs/user/dashboard/lens-end-to-end/images/non-edited/lens_end_to_end_1.png deleted file mode 100644 index 49b917753b7ef..0000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/non-edited/lens_end_to_end_1.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/non-edited/lens_end_to_end_2.png b/docs/user/dashboard/lens-end-to-end/images/non-edited/lens_end_to_end_2.png deleted file mode 100644 index 64330de64ca78..0000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/non-edited/lens_end_to_end_2.png and /dev/null differ diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index ca651ed16189a..58476bcae87df 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -1,436 +1,222 @@ -[[create-a-dashboard-of-panels-with-web-server-data]] -== Tutorial: Create a dashboard of panels with web server data +[[lens]] +=== Lens -You collected data from your web server, and you want to visualize and analyze the data on a dashboard. To create dashboard panels of the data, open the *Lens* visualization builder, then -create the visualization panels that best display the data. +To create visualizations with *Lens*, you drag and drop data fields onto the visualization builder, +then *Lens* uses heuristics to apply each field. -[discrete] -[[add-the-data-and-create-the-dashboard]] -=== Add the data and create the dashboard +[role="screenshot"] +image:dashboard/images/lens.png[Lens] -To create visualizations of the data from the web server, add the data set, then create the dashboard. +[float] +[[lens-required-choices]] +==== Open and set up Lens -. From the {kib} *Home* page, click *Try our sample data*. +Open *Lens*, then explore the fields in your data. The list of fields are determined by the index pattern and time filter. -. From *Sample web logs*, click *Add data*. +. On the dashboard, click *Create panel*. -. Open the main menu, click *Dashboard*. +. On the *New visualization* window, click *Lens*. -. Click *Create dashboard*. +. <>. -[float] -[[open-and-set-up-lens]] -=== Open and set up Lens +. To view the fields in the a different index pattern, click the index pattern, then select a different index pattern from the dropdown. -With *Lens*, you identify the data fields you want to visualize, drag and drop the fields, then watch as -*Lens* uses heuristics to apply the fields and create a visualization for you. +. Scan through the list of fields to see what’s in your data. ++ +TIP: For sparse datasets, *Empty fields* can contain data, which you can use to create visualizations. -. From the dashboard, click *Create panel*. +. To filter the data fields, use the following options: -. On the *New visualization* window, click *Lens*. -+ -[role="screenshot"] -image::images/lens_end_to_end_1_1.png[New visualization popover] +* Enter the field name in the *Search field names* field. -. Make sure the *kibana_sample_data_logs* index appears. -+ -The list of fields are dependent on the <>, <>, and field filters. +* Click *Field by type*, then select the filter. To show all fields in the index pattern, deselect *Only show fields with data*. [float] [[view-the-data-summaries]] -=== View the data summaries +===== View the data summaries -For each field, *Lens* shows a summary depending on the type of data. Date fields show the time distribution, string fields show the top 10 values, -and numeric fields show a detailed summary with the top 10 values and a value distribution. +For each field, *Lens* displays a summary depending on the type of data. To analyze the fields, *Lens* uses a sample of 5,000 documents. +Each summary displays the percentage of sampled documents over all available documents. -To view the data summary for a field, click *i* next to the field. +To view a data summary, click *i* next to the field. [role="screenshot"] image::images/lens_data_info_documents.png[Data summary analyzed documents] -*Lens* uses a sample of 5,000 documents to perform the field analysis. The bottom line of the summary shows the percentage of sampled documents over all available documents. +Each data type display the following summaries: -When *Lens* presents the top 10 values distribution, it also shows the percentage of *Other* values. For array value fields, the percentage distribution considers each value in the array as separate. +* *Date* — Displays the time distribution. -[role="screenshot"] -image::images/lens_data_info.png[Data summary window with Other] - -NOTE: The sum of all the *Other* fields can equal more than 100% by a small amount. - -[discrete] -[[view-the-number-of-website-visitors]] -=== View the number of website visitors - -To determine how many users have visited your website within the last 90 days, create a metric visualization, then add it to the dashboard. +* *String* — Displays the top 10 values. -. Set the <> to *Last 90 days*. +* *Numeric* — Displays the top 10 values and a value distribution. When *Lens* displays the top 10 values distribution, the percentage of *Other* values is also displayed. +For array value fields, the percentage distribution considers each value in the array as separate. -. From the *Chart Type* dropdown, select *Metric*. -+ -[role="screenshot"] -image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected] +TIP: *Other* can equal more than 100% by a small amount. -. From the *Available fields* list, drag and drop *clientip* to the visualization builder. -+ -[role="screenshot"] -image::images/lens_end_to_end_1_3.png[Changed type and dropped clientip field] - -. From the editor, click *Unique count of clientip*. - -.. In the *Display name* field, enter `Unique visitors`. - -.. Click *Close*. -+ -[role="screenshot"] -image::images/lens_end_to_end_1_4.png[Flyout config open] - -. *Save* the panel, enter `Unique visitors` in the *Title* field, then add the panel to the dashboard. - -[discrete] -[[view-the-distribution-of-visitors-by-operating-system]] -=== View the distribution of visitors by operating system - -To determine the operating systems you should continue to support, and the importance of mobile traffic from iOS devices, -create a donut chart that displays the top operating systems that your visitors used to access your website within the last 90 days. - -. Open *Lens*, then set the <> to *Last 90 days*. - -. From the *Chart Type* dropdown, select *Donut*. +[float] +[[create-the-visualization-panel]] +==== Create the visualization panel -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. +Drag and drop the fields on to the visualization builder, then -.. Drag and drop *clientip* to the *Size by* field. +. Drag and drop the fields to the visualization builder. -.. Drag and drop *machine.os.keyword* to the *Slice by* field. -+ -[role="screenshot"] -image::images/lens_end_to_end_2_1_1.png[Donut chart with clientip and machine.os.keyword fields] +. To change the visualization type, use the following options: -. Change the color palette. +* Click the *Suggestions*. -.. From the editor, click *Top values of machine.os.keyword*. +* From the *Chart Type* dropdown, click the visualization. -.. From the *Color palette* dropdown, select *Compatibility*. +. To configure the data, use the editor, then click *Close*. -.. Click *Close*. +. To configure the visualization, use the dropdown menus. + [role="screenshot"] -image::images/lens_end_to_end_2_1.png[Donut chart with open config panel] - -. *Save* the panel, enter `Visitors by OS` in the *Title* field, then add the panel to the dashboard. - -[discrete] -[[mixed-multiaxis]] -=== View the average of bytes transfer per day - -To prevent potential server failures, and optimize the cost of website maintenance, create an area chart that displays the average of bytes transfer. To compare -the data to the number of visitors to your website, add a line chart layer. +image::images/lens_value_labels_xychart_toggle.png[Lens Bar chart value labels menu] -. Open *Lens*. +[float] +[[drag-and-drop-keyboard-navigation]] +===== Create visualization panels with keyboard navigation -. From the *Available fields* list, drag and drop *bytes* to the visualization builder. +*Lens* has a fully accessible, continuously improved drag and drop system, which allows you to use a keyboard instead of a mouse. -. To zoom in on the data you want to view, click and drag your cursor across the bars. -+ [role="screenshot"] -image::images/lens_end_to_end_3_1_1.gif[Zoom in on the data] - -. Change the *timestamp* interval. - -.. From the editor, click *timestamp*. - -.. Select *Customize time interval*. +image::images/lens_drag_drop_1.gif[Presented Lens drag and drop] -.. Change the *Minimum interval* to `1 days`, then click *Close*. +. Set a focus on the chosen item. Most of the draggable elements have two focus states. The inner focus state opens a panel with detailed information or options. +The outer focus state allows you to drag an item. Tab through the page until you get the outer focus state on the chosen item: + [role="screenshot"] -image::images/lens_end_to_end_3_1.png[Customize time interval] +image::images/lens_drag_drop_2.png[Lens drag and drop focus state] -. From the *Chart Type* dropdown, select *Area*. +. Complete the following actions: -[discrete] -[[add-a-data-layer]] -==== Add the line chart layer +* To start dragging an item, press Space bar. -To compare the average of bytes transfer to the number of users that visit your website, add a line chart layer. +* To select where you want to drop the item, use the Left and Right arrows. -. From the editor, click *+*. -+ -[role="screenshot"] -image::images/lens_end_to_end_3_2.png[Add new layer button] +* To reorder the fields in the group, use Up and Down arrows. -. From the new layer editor, click the *Chart type* dropdown, then click the line chart. +* To duplicate an operation, use the Left and Right arrows to select the `Drop a field or click to add` in the same group. + [role="screenshot"] -image::images/lens_end_to_end_3_3.png[Change layer type] -+ -The chart type for the visualization changes to *Mixed XY*. - -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. - -.. Drag and drop *timestamp* to the *Horizontal axis* field. - -.. Drag and drop *clientip* to the *Vertical axis* field. - -. Change the *timestamp* interval. - -.. From the editor, click *timestamp* in the line chart layer. - -.. Select *Customize time interval*. - -.. Change the *Minimum interval* to `1 days`, then click *Close*. - -. Change the *Unique count of clientip* label and color. - -.. From the editor, click *Unique count of clientip*. +image::images/lens_drag_drop_3.gif[Using drag and drop to reorder] -.. In the *Display name* field, enter `Unique visitors` in the line chart layer. - -.. In the *Series color* field, enter *#CA8EAE*, then click *Close*. - -[discrete] -[[configure-the-multiaxis-chart]] -==== Configure the y-axes - -There is a significant difference between the *timestamp per day* and *Unique visitors* data, which makes the *Unique visitors* data difficult to read. To improve the readability, -display the *Unique visitors* data along a second y-axis, then change the formatting. When functions contain multiple formats, separate axes are created by default. - -. From the editor, click *Unique visitors* in the line chart layer. - -. For *Axis side*, click *Right*, then click *Close*. +. Press Space bar to confirm, or to cancel, press Esc. [float] -[[change-the-visualization-type]] -==== Change the visualization type - -. From the editor, click *Average of bytes* in the area chart layer. - -. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. -+ -[role="screenshot"] -image::images/lens_end_to_end_3_4.png[Multiaxis chart] - -[discrete] -[[lens-legend-position]] -==== Change the legend position +[[lens-faq]] +==== Frequently asked questions -The visualization is done, but the legend uses a lot of space. Change the legend position to the top of the chart. +For the answers to common *Lens* questions, review the following. -. From the *Legend* dropdown, select the top position. -+ -[role="screenshot"] -image::images/lens_end_to_end_3_5.png[legend position] +[float] +[[kql-]] +===== When should I use the Filter function instead of KQL filters? -. *Save* the panel, enter `Average Bytes vs. Unique Visitors` in the *Title* field, then add the panel to the dashboard. +The easiest way to apply KQL filters is to use <>, but you can also use the *Filters* function in the following scenarios: -[discrete] -[[percentage-stacked-area]] -=== View the health of your website +* When you want to apply more than one KQL filter to the visualization. -To detect unusual traffic, bad website links, and server errors, create a percentage stacked area chart that displays the associated response codes. +* When you want to apply the KQL filter to a single layer, which allows you to visualize filtered and unfiltered data. -. Open *Lens*. +[float] +[[when-should-i-normalize-the-data-by-unit-or-use-a-custom-interval]] +===== When should I normalize the data by unit or use a custom interval? -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. +* *Normalize by unit* — Calculates the average for the specified interval. When you normalize the data by unit, the data appears less granular, but *Lens* is able to calculate the data faster. -.. Drag and drop *Records* to the *Vertical axis* field. +* *Customize time interval* — Creates a bucket for each specified interval. When you customize the time interval, you can use a large time range, but *Lens* calculates the data slower. -.. Drag and drop *@timestamp* to the *Horizontal axis* field. +To normalize the interval: -. From the *Chart Type* dropdown, select *Percentage bar*. +. In the editor, click a field. -. To remove the vertical axis label, click *Left axis*, then deselect *Show*. -+ -[role="screenshot"] -image::images/lens_end_to_end_4_3.png[Turn off axis name] +. Click *Add advanced options > Normalize by unit*. -[discrete] -[[add-the-response-code-filters]] -==== Add the response code filters +. From the *Normalize by unit* dropdown, select an option, then click *Close*. -For each response code that you want to display, create a filter. +To create a custom interval: -. From the editor, click the *Drop a field or click to add* field for *Break down by*. +. In the editor, click a field. -. From *Select a function*, click *Filters*. +. Select *Customize time interval*. -. Add the filter for the successful response codes. +. Change the *Minimum interval*, then click *Close*. -.. Click *All records*. +[float] +[[can-i-show-value-labels-for-my-chart]] +===== How do I display value labels? -.. In the *KQL* field, enter `response.keyword>=200 AND response.keyword<300`. +A subset of *Lens* visualizations support value labels. -.. In the *Label* field, enter `2XX`, then press Return. +* *Bar* and *Horizontal Bar* + [role="screenshot"] -image::images/lens_end_to_end_4_1.png[First filter in filters aggregation] - -. Add the filter for the redirect codes. - -.. Click *Add a filter*. - -.. In the *KQL* field, enter `response.keyword>=300 AND response.keyword<400`. - -.. In the *Label* field, enter `3XX`, then press Return. - -. Add the filter for the client error codes. - -.. Click *Add a filter*. - -.. In the *KQL* field, enter `response.keyword>=400 AND response.keyword<500`. - -.. In the *Label* field, enter `4XX`, then press Return. - -. Add the filter for the server error codes. - -.. Click *Add a filter*. - -.. In the *KQL* field, enter `response.keyword>=500 AND response.keyword<600`. - -.. In the *Label* field, enter `5XX`, then press Return. - -. To change the color palette, select *Status* from the *Color palette* dropdown, then click *Close*. - -. *Save* the panel, enter `Response Codes Over Time` in the *Title* field, then add the panel to the dashboard. - -[discrete] -[[histogram]] -=== View the traffic for your website by the hour - -To find the best time to shut down your website for maintenance, create a histogram that displays the traffic for your website by the hour. +image::images/lens_value_labels_xychart_toggle.png[Lens Bar chart value labels menu] -. Open *Lens*. - -. From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. - -.. Click *Average of bytes*. - -.. From *Select a function*, click *Sum*. - -.. In the *Display name* field, enter `Transferred bytes`. - -.. From the *Value format* dropdown, select `Bytes (1024)`, then click *Close*. - -. From the *Available fields* list, drag and drop *hour_of_day* to *Horizontal axis* in the editor, then configure the options. - -.. Click *hour_of_day*. - -.. Click and slide the *Intervals granularity* slider until the horizontal axis displays hourly intervals. +* *Pie*, *Donut*, and *Treemap* + [role="screenshot"] -image::images/lens_end_to_end_5_2.png[Create custom ranges] +image::images/lens_value_labels_partition_toggle.png[Lens Pie chart value labels menu] -. *Save* the panel, enter `Hourly Traffic Distribution` in the *Title* field, then add the panel to the dashboard. - -[discrete] -[[custom-ranges]] -=== View the percent of small versus large transferred files - -To determine if your users transfer more small files versus large files, create a pie chart that displays the percentage of each size. - -. Open *Lens*. - -. From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. - -.. Click *Average of bytes*. - -.. From *Select a function*, click *Sum*, then click *Close*. - -. From the *Available fields* list, drag and drop *bytes* to *Break down by* in the editor, then specify the file size ranges. - -.. Click *bytes*. +[float] +[[what-is-the-other-category]] +===== What data is categorized as Other? -.. Click *Create custom ranges*, enter the following, then press Return: +The *Other* category contains all of the documents that do not match the specified criteria or filters. +Use *Other* when you want to compare a value, or multiple values, to a whole. +By default, *Group other values as "Other"* is enabled when you select the *Top values* function. -* *Ranges* — `0` -> `10240` +To disable *Group other values as "Other"*: -* *Label* — `Below 10KB` +. In the editor, click *Advanced*. -.. Click *Add range*, enter the following, then press Return: +. Deselect *Group other values as "Other"*. -* *Ranges* — `10240` -> `+∞` +[float] +[[how-can-i-include-documents-without-the-field-in-the-operation]] +===== How can I include documents without the field? -* *Label* — `Above 10KB` -+ -[role="screenshot"] -image::images/lens_end_to_end_6_1.png[Custom ranges configuration] +By default, *Lens* retrieves only the documents from the specified field. +For bucket aggregations, such as *Top values*, you can choose to include documents that do not contain the specified field, +which is helpful when you want to compare to the whole documentation set. -.. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. +. In the editor, click *Advanced*. -. From the *Chart Type* dropdown, select *Pie*. +. Select *Include documents without this field*. + [role="screenshot"] -image::images/lens_end_to_end_6_2.png[Files size distribution] - -. *Save* the panel, enter `File size distribution` in the *Title* field, then add the panel to the dashboard. - -[discrete] -[[treemap]] -=== View the top sources of website traffic - -To determine how users find out about your website and where your users are located, create a treemap that displays the percentage of users that -enter your website from specific social media websites, and the top countries where users are located. - -. Open *Lens*. - -. From the *Chart Type* dropdown, select *Treemap*. - -. From the *Available fields* list, drag and drop *Records* to the *Size by* field in the editor. - -. From the editor, click the *Drop a field or click to add* field for *Group by*, then create a filter for each website traffic source. - -.. From *Select a function*, click *Filters*. - -.. Click *All records*, enter the following, then press Return: - -* *KQL* — `referer : *facebook.com*` - -* *Label* — `Facebook` - -.. Click *Add a filter*, enter the following, then press Return: - -* *KQL* — `referer : *twitter.com*` +image::images/lens_bucketed_aggregation_advanced_dropdown.png[Lens Advanced options for bucketed aggregations] -* *Label* — `Twitter` - -.. Click *Add a filter*, enter the following, then press Return: - -* *KQL* — `NOT referer : *twitter* OR NOT referer: *facebook.com*` - -* *Label* — `Other` - -.. Click *Close*. - -[discrete] -[[add-the-countries]] -==== Add the geographic data - -To determine the top countries where users are located, add the geographic data. +[float] +[[is-it-possible-to-select-color-for-specific-bar-or-point]] +===== How do I change the color for a single data point? -Compare the top sources of website traffic data to the top three countries. +*Lens* provides you with color pallettes that you can apply to the entire visualization, but you are unable to change the color for a single data point, such as a bar or line. -. From the *Available fields* list, drag and drop *geo.src* to the visualization builder. +[float] +[[can-i-sort-by-multiple-columns]] +===== How do I sort by multiple columns? -. To change the *Group by* order, click and drag *Top values of geo.src* so that it appears first in the editor. -+ -[role="screenshot"] -image::images/lens_end_to_end_7_2.png[Treemap vis] +Multiple column sorting is unsupported in *Lens*, but is supported in *Discover*. For information on how to sort multiple columns in *Discover*, +refer to <>. -. To view only the Facebook and Twitter data, remove the *Other* category. +[float] +[[is-it-possible-to-sort-dimensions-in-a-chart]] +===== How do I sort the dimensions in a chart? -.. From the editor, click *Top values of geo.src*. +Sorting dimensions in visualizations is unsupported in *Lens*. -.. From the *Advanced* dropdown, deselect *Group other values as "Other"*, then click *Close*. -+ -[role="screenshot"] -image::images/lens_end_to_end_7_3.png[Group other values as Other] +[float] +[[is-it-possible-to-use-saved-serches-in-lens]] +===== How do I visualize saved searches? -. *Save* the panel, enter `Traffic Source For Top 3 Countries` in the *Title* field, then add the panel to the dashboard. +Visualizing saved searches in unsupported in *Lens*. [float] -== What's next? +[[is-it-possible-to-decrease-or-increase-the-number-of-suggestions]] +===== How do I change the number of suggestions? -Your dashboard is complete and provides you with an overall picture of the data from your web server. - -[role="screenshot"] -image::images/lens_end_to_end_dashboard.png[Final dashboard vis] \ No newline at end of file +Configuring the *Suggestions* that *Lens* automatically populates is unsupported. diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index f785f147d04e0..941f78168ecf7 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -1,4 +1,4 @@ -[[create-panels-with-timelion]] +[[timelion]] === Timelion Instead of using a visual editor to create charts, you define a graph by chaining functions together, using the *Timelion*-specific syntax. diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 0fa0b00ad040f..f94048a836a1f 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -1,4 +1,4 @@ -[[TSVB]] +[[tsvb]] === TSVB *TSVB* enables you to visualize the data from multiple data series, supports <> or <>. +Performing math across data series is unsupported in *TSVB*. To calculate the difference between two data series, use <> or <>. [float] ===== How do I compare the current versus previous month? @@ -153,7 +153,7 @@ The ability to calculate a month over month change is not fully supported in *TS time filter is set to 3 months or more _and_ the *Interval* is `1m`. Use the *Derivative* to get the absolute monthly change. To convert to a percent, add the *Math* function with the `params.current / (params.current - params.derivative)` formula, then select *Percent* from the *Data Formatter* dropdown. -For other types of month over month calculations, use <> or <>. +For other types of month over month calculations, use <> or <>. [float] ===== How do I calculate the duration between the start and end of an event? diff --git a/docs/user/dashboard/lens-end-to-end/lens-end-to-end.asciidoc b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc similarity index 61% rename from docs/user/dashboard/lens-end-to-end/lens-end-to-end.asciidoc rename to docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc index f111f602de849..22483b2801848 100644 --- a/docs/user/dashboard/lens-end-to-end/lens-end-to-end.asciidoc +++ b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc @@ -1,43 +1,81 @@ -[float] -[[lens-end-to-end]] -== Tutorial: Analyze website data on a dashboard +[[create-a-dashboard-of-panels-with-web-server-data]] +== Tutorial: Create a dashboard of panels with web server data -Most dashboards use similar types of analysis to answer key questions. In this tutorial you will base your analysis off of {kib} sample data which resembles logs from the Apache web server. +You collected data from your web server, and you want to visualize and analyze the data on a dashboard. To create dashboard panels of the data, open the *Lens* visualization builder, then +create the visualization panels that best display the data. -When you're finished creating the dashboard, you'll be able to answer the following questions: +When you've completed the tutorial, you'll have a dashboard that provides you with a complete overview of your web server data. -* How many users have visited your website? -* What is the distribution of visitors by operating system? -* What is the average of bytes transfer per day? -* What is the health of your website? -* What is the traffic for your website by the hour? -* What is the percentage of small compared to large transferred files? -* What are the top social media sources of website traffic, and from what countries? +[role="screenshot"] +image::images/lens_end_to_end_dashboard.png[Final dashboard vis] [discrete] -[[add-the-sample-web-logs-data]] +[[add-the-data-and-create-the-dashboard]] === Add the data and create the dashboard -To create visualizations of the website traffic data, add the data set, then create the dashboard. +To create visualizations of the data from the web server, add the data set, then create the dashboard. -. From the *Home* page, click *Try our sample data*. +. From the {kib} *Home* page, click *Try our sample data*. . From *Sample web logs*, click *Add data*. . Open the main menu, click *Dashboard*. -. On the *Dashboards* page, click *Create dashboard*. +. Click *Create dashboard*. + +[float] +[[open-and-set-up-lens]] +=== Open and set up Lens + +With *Lens*, you identify the data fields you want to visualize, drag and drop the fields, then watch as +*Lens* uses heuristics to apply the fields and create a visualization for you. + +. From the dashboard, click *Create panel*. + +. On the *New visualization* window, click *Lens*. ++ +[role="screenshot"] +image::images/lens_end_to_end_1_1.png[New visualization popover] + +. Make sure the *kibana_sample_data_logs* index appears. + +[discrete] +[[view-the-number-of-website-visitors]] +=== View the number of website visitors + +To determine how many users have visited your website within the last 90 days, create a metric visualization, then add it to the dashboard. + +. Set the <> to *Last 90 days*. + +. From the *Chart Type* dropdown, select *Metric*. ++ +[role="screenshot"] +image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected] + +. From the *Available fields* list, drag and drop *clientip* to the visualization builder. ++ +[role="screenshot"] +image::images/lens_end_to_end_1_3.png[Changed type and dropped clientip field] + +. In the editor, click *Unique count of clientip*. + +.. In the *Display name* field, enter `Unique visitors`. + +.. Click *Close*. ++ +[role="screenshot"] +image::images/lens_end_to_end_1_4.png[Flyout config open] + +. Click *Save and return*. [discrete] -[[donut-vis]] +[[view-the-distribution-of-visitors-by-operating-system]] === View the distribution of visitors by operating system To determine the operating systems you should continue to support, and the importance of mobile traffic from iOS devices, -create a donut chart that displays the top operating systems that your visitors use to access your webiste. - -. From the dashboard, click *Create panel*, then click *Lens* on the *New visualization* window. +create a donut chart that displays the top operating systems that your visitors used to access your website within the last 90 days. -. Make sure the *kibana_sample_data_logs* index appears, and the <> is set to *Last 90 days*. +. Open *Lens*, then set the <> to *Last 90 days*. . From the *Chart Type* dropdown, select *Donut*. @@ -52,35 +90,25 @@ image::images/lens_end_to_end_2_1_1.png[Donut chart with clientip and machine.os . Change the color palette. -.. From the editor, click *Top values of machine.os.keyword*. +.. In the editor, click *Top values of machine.os.keyword*. -.. From the *Color palette* dropdown, select *Compability*. +.. From the *Color palette* dropdown, select *Compatibility*. .. Click *Close*. + [role="screenshot"] image::images/lens_end_to_end_2_1.png[Donut chart with open config panel] -. Save the visualization, then add it to the dashboard. - -.. From the toolbar, click *Save*. - -.. In the *Title* field, enter `Visitors by OS`. - -.. Select *Add to dashboard after saving*. - -.. Click *Save and return*. +. Click *Save and return*. [discrete] [[mixed-multiaxis]] === View the average of bytes transfer per day -To prevent potential server failures and optimize the cost of website maintenance, create an area chart that displays the average of bytes transfer, -then add a line chart layer to compare the data to the number of visitors to your website. - -. From the dashboard, click *Create panel*, then click *Lens* on the *New visualization* window. +To prevent potential server failures, and optimize the cost of website maintenance, create an area chart that displays the average of bytes transfer. To compare +the data to the number of visitors to your website, add a line chart layer. -. Make sure the *kibana_sample_data_logs* index appears. +. Open *Lens*. . From the *Available fields* list, drag and drop *bytes* to the visualization builder. @@ -91,17 +119,15 @@ image::images/lens_end_to_end_3_1_1.gif[Zoom in on the data] . Change the *timestamp* interval. -.. From the editor, click *timestamp*. +.. In the editor, click *timestamp*. .. Select *Customize time interval*. -.. Change the *Minimum interval* to `1 days`. +.. Change the *Minimum interval* to `1 days`, then click *Close*. + [role="screenshot"] image::images/lens_end_to_end_3_1.png[Customize time interval] -.. Click *Close*. - . From the *Chart Type* dropdown, select *Area*. [discrete] @@ -110,7 +136,7 @@ image::images/lens_end_to_end_3_1.png[Customize time interval] To compare the average of bytes transfer to the number of users that visit your website, add a line chart layer. -. From the editor, click *+*. +. In the editor, click *+*. + [role="screenshot"] image::images/lens_end_to_end_3_2.png[Add new layer button] @@ -130,23 +156,19 @@ The chart type for the visualization changes to *Mixed XY*. . Change the *timestamp* interval. -.. From the editor, click *timestamp* in the line chart layer. +.. In the editor, click *timestamp* in the line chart layer. .. Select *Customize time interval*. -.. Change the *Minimum interval* to `1 days`. - -.. Click *Close*. +.. Change the *Minimum interval* to `1 days`, then click *Close*. . Change the *Unique count of clientip* label and color. -.. From the editor, click *Unique count of clientip*. +.. In the editor, click *Unique count of clientip*. .. In the *Display name* field, enter `Unique visitors` in the line chart layer. -.. In the *Series color* field, enter *#CA8EAE*. - -.. Click *Close*. +.. In the *Series color* field, enter *#CA8EAE*, then click *Close*. [discrete] [[configure-the-multiaxis-chart]] @@ -155,41 +177,33 @@ The chart type for the visualization changes to *Mixed XY*. There is a significant difference between the *timestamp per day* and *Unique visitors* data, which makes the *Unique visitors* data difficult to read. To improve the readability, display the *Unique visitors* data along a second y-axis, then change the formatting. When functions contain multiple formats, separate axes are created by default. -. From the editor, click *Unique visitors* in the line chart layer. +. In the editor, click *Unique visitors* in the line chart layer. -.. For *Axis side*, click *Right*. +. For *Axis side*, click *Right*, then click *Close*. -.. Click *Close*. +[float] +[[change-the-visualization-type]] +==== Change the visualization type -. From the editor, click *Average of bytes* in the area chart layer. +. In the editor, click *Average of bytes* in the area chart layer. -.. From the *Value format* dropdown, select *Bytes (1024)*. +. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. + [role="screenshot"] image::images/lens_end_to_end_3_4.png[Multiaxis chart] -.. Click *Close*. - [discrete] [[lens-legend-position]] -==== Change the legend position and save the visualization +==== Change the legend position -The visualization is done, but the legend uses a lot of space. Change the legend position to the top of the chart, then save the visualization and add it to the dashboard. +The visualization is done, but the legend uses a lot of space. Change the legend position to the top of the chart. . From the *Legend* dropdown, select the top position. + [role="screenshot"] image::images/lens_end_to_end_3_5.png[legend position] -. Save the visualization, then add it to the dashboard. - -.. From the toolbar, click *Save*. - -.. In the *Title* field, enter `Average Bytes vs. Unique Visitors`. - -.. Select *Add to dashboard after saving*. - -.. Click *Save and return*. +. Click *Save and return*. [discrete] [[percentage-stacked-area]] @@ -197,9 +211,7 @@ image::images/lens_end_to_end_3_5.png[legend position] To detect unusual traffic, bad website links, and server errors, create a percentage stacked area chart that displays the associated response codes. -. From the dashboard, click *Create panel*, then click *Lens* on the *New visualization* window. - -. Make sure the *kibana_sample_data_logs* index appears. +. Open *Lens*. . From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. @@ -220,7 +232,7 @@ image::images/lens_end_to_end_4_3.png[Turn off axis name] For each response code that you want to display, create a filter. -. From the editor, click the *Drop a field or click to add* field for *Break down by*. +. In the editor, click the *Drop a field or click to add* field for *Break down by*. . From *Select a function*, click *Filters*. @@ -230,13 +242,11 @@ For each response code that you want to display, create a filter. .. In the *KQL* field, enter `response.keyword>=200 AND response.keyword<300`. -.. In the *Label* field, enter `2XX`. +.. In the *Label* field, enter `2XX`, then press Return. + [role="screenshot"] image::images/lens_end_to_end_4_1.png[First filter in filters aggregation] -.. Press Return. - . Add the filter for the redirect codes. .. Click *Add a filter*. @@ -261,19 +271,9 @@ image::images/lens_end_to_end_4_1.png[First filter in filters aggregation] .. In the *Label* field, enter `5XX`, then press Return. -. To change the color pallette, select *Status* from the *Color palette* dropdown. - -.. Click *Close*. - -. Save the visualization, then add it to the dashboard. - -.. From the toolbar, click *Save*. - -.. In the *Title* field, enter `Response Codes Over Time`. +. To change the color palette, select *Status* from the *Color palette* dropdown, then click *Close*. -.. Select *Add to dashboard after saving*. - -.. Click *Save and return*. +. Click *Save and return*. [discrete] [[histogram]] @@ -281,9 +281,7 @@ image::images/lens_end_to_end_4_1.png[First filter in filters aggregation] To find the best time to shut down your website for maintenance, create a histogram that displays the traffic for your website by the hour. -. From the dashboard, click *Create panel*, then click *Lens* on the *New visualization* window. - -. Make sure the *kibana_sample_data_logs* index appears. +. Open *Lens*. . From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. @@ -293,9 +291,7 @@ To find the best time to shut down your website for maintenance, create a histog .. In the *Display name* field, enter `Transferred bytes`. -.. From the *Value format* dropdown, select `Bytes (1024)`. - -.. Click *Close*. +.. From the *Value format* dropdown, select `Bytes (1024)`, then click *Close*. . From the *Available fields* list, drag and drop *hour_of_day* to *Horizontal axis* in the editor, then configure the options. @@ -306,15 +302,7 @@ To find the best time to shut down your website for maintenance, create a histog [role="screenshot"] image::images/lens_end_to_end_5_2.png[Create custom ranges] -. Save the visualization, then add it to the dashboard. - -.. From the toolbar, click *Save*. - -.. In the *Title* field, enter `Hourly Traffic Distribution`. - -.. Select *Add to dashboard after saving*. - -.. Click *Save and return*. +. Click *Save and return*. [discrete] [[custom-ranges]] @@ -322,17 +310,13 @@ image::images/lens_end_to_end_5_2.png[Create custom ranges] To determine if your users transfer more small files versus large files, create a pie chart that displays the percentage of each size. -. From the dashboard, click *Create panel*, then click *Lens* on the *New visualization* window. - -. Make sure the *kibana_sample_data_logs* index appears. +. Open *Lens*. . From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. .. Click *Average of bytes*. -.. From *Select a function*, click *Sum*. - -.. Click *Close*. +.. From *Select a function*, click *Sum*, then click *Close*. . From the *Available fields* list, drag and drop *bytes* to *Break down by* in the editor, then specify the file size ranges. @@ -353,24 +337,14 @@ To determine if your users transfer more small files versus large files, create [role="screenshot"] image::images/lens_end_to_end_6_1.png[Custom ranges configuration] -.. From the *Value format* dropdown, select *Bytes (1024)*. - -.. Click *Close*. +.. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. . From the *Chart Type* dropdown, select *Pie*. + [role="screenshot"] image::images/lens_end_to_end_6_2.png[Files size distribution] -. Save the visualization, then add it to the dashboard. - -.. From the toolbar, click *Save*. - -.. In the *Title* field, enter `File size distribution`. - -.. Select *Add to dashboard after saving*. - -.. Click *Save and return*. +. Click *Save and return*. [discrete] [[treemap]] @@ -379,15 +353,13 @@ image::images/lens_end_to_end_6_2.png[Files size distribution] To determine how users find out about your website and where your users are located, create a treemap that displays the percentage of users that enter your website from specific social media websites, and the top countries where users are located. -. From the dashboard, click *Create panel*, then click *Lens* on the *New visualization* window. - -. Make sure the *kibana_sample_data_logs* index appears. +. Open *Lens*. . From the *Chart Type* dropdown, select *Treemap*. . From the *Available fields* list, drag and drop *Records* to the *Size by* field in the editor. -. From the editor, click the *Drop a field or click to add* field for *Group by*, then create a filter for each website traffic source. +. In the editor, click the *Drop a field or click to add* field for *Group by*, then create a filter for each website traffic source. .. From *Select a function*, click *Filters*. @@ -415,7 +387,7 @@ enter your website from specific social media websites, and the top countries wh [[add-the-countries]] ==== Add the geographic data -To determine the top countries where users are located, add the geographic data, then save and add the visualization to the dashboard. +To determine the top countries where users are located, add the geographic data. Compare the top sources of website traffic data to the top three countries. @@ -428,26 +400,20 @@ image::images/lens_end_to_end_7_2.png[Treemap vis] . To view only the Facebook and Twitter data, remove the *Other* category. -.. From the editor, click *Top values of geo.src*. +.. In the editor, click *Top values of geo.src*. -.. From the *Advanced* dropdown, deselect *Group other values as "Other"*. +.. From the *Advanced* dropdown, deselect *Group other values as "Other"*, then click *Close*. + [role="screenshot"] image::images/lens_end_to_end_7_3.png[Group other values as Other] -.. Click *Close*. - -. Save the visualization, then add it to the dashboard. +. Click *Save and return*. -.. From the toolbar, click *Save*. - -.. In the *Title* field, enter `Traffic Source For Top 3 Countries`. - -.. Select *Add to dashboard after saving*. +[discrete] +=== Save the dashboard -.. Click *Save and return*. +Now that you have a complete overview of your web server data, save the dashboard. -That's it! You've created a dashboard that provides you with a complete picture of your website data. +. In the toolbar, click *Save*. -[role="screenshot"] -image::images/lens_end_to_end_dashboard.png[Final dashboard vis] +. On the *Save dashboard* window, enter `Web server data`, then click *Save*. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 3ee3c04f7b216..974c8febbe276 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -17,10 +17,9 @@ Consult your administrator if you do not have the appropriate access. [cols="50, 50"] |=== -| <> -| Create and manage {es} -pipelines that enable you to perform common transformations and -enrichments on your data. +| {ref}/ingest.html[Ingest Node Pipelines] +| Create and manage ingest pipelines that let you perform common transformations +and enrichments on your data. | {logstash-ref}/logstash-centralized-pipeline-management.html[Logstash Pipelines] | Create, edit, and delete your Logstash pipeline configurations. @@ -187,8 +186,6 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] -include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] - include::{kib-repo-dir}/management/managing-fields.asciidoc[] include::{kib-repo-dir}/management/managing-licenses.asciidoc[] diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index bdc0724eae9af..710ad069027c8 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -121,6 +121,7 @@ export class DocLinksService { indexPatterns: { loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`, introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, + fieldFormattersNumber: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/numeral.html`, }, addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, @@ -354,6 +355,7 @@ export interface DocLinksStart { readonly indexPatterns: { readonly loadingData: string; readonly introduction: string; + readonly fieldFormattersNumber: string; }; readonly addData: string; readonly kibana: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index e29173d1495af..2759a0cd7c6b9 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -571,6 +571,7 @@ export interface DocLinksStart { readonly indexPatterns: { readonly loadingData: string; readonly introduction: string; + readonly fieldFormattersNumber: string; }; readonly addData: string; readonly kibana: string; diff --git a/src/core/server/ui_settings/saved_objects/migrations.ts b/src/core/server/ui_settings/saved_objects/migrations.ts index 7ea5573076ba0..850fc6d7d46c0 100644 --- a/src/core/server/ui_settings/saved_objects/migrations.ts +++ b/src/core/server/ui_settings/saved_objects/migrations.ts @@ -28,4 +28,32 @@ export const migrations = { }), references: doc.references || [], }), + '7.12.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({ + ...doc, + ...(doc.attributes && { + attributes: Object.keys(doc.attributes).reduce((acc, key) => { + if (key === 'timepicker:quickRanges' && doc.attributes[key].indexOf('section') > -1) { + const ranges = JSON.parse(doc.attributes[key]).map( + ({ from, to, display }: { from: string; to: string; display: string }) => { + return { + from, + to, + display, + }; + } + ); + return { + ...acc, + 'timepicker:quickRanges': JSON.stringify(ranges, null, 2), + }; + } else { + return { + ...acc, + [key]: doc.attributes[key], + }; + } + }, {}), + }), + references: doc.references || [], + }), }; diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index a8c9b9144707d..ad4d7ff8d78e2 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -31,6 +31,20 @@ describe('filterMatchesIndex', () => { expect(filterMatchesIndex(filter, indexPattern)).toBe(true); }); + it('should return true if custom filter for the same index is passed', () => { + const filter = { meta: { index: 'foo', key: 'bar', type: 'custom' } } as Filter; + const indexPattern = { id: 'foo', fields: [{ name: 'bara' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + }); + + it('should return false if custom filter for a different index is passed', () => { + const filter = { meta: { index: 'foo', key: 'bar', type: 'custom' } } as Filter; + const indexPattern = { id: 'food', fields: [{ name: 'bara' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(false); + }); + it('should return false if the filter key does not match a field name', () => { const filter = { meta: { index: 'foo', key: 'baz' } } as Filter; const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 9fd8567b76e2b..478263d5ce601 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -18,5 +18,12 @@ export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern if (!filter.meta?.key || !indexPattern) { return true; } + + // Fixes https://github.com/elastic/kibana/issues/89878 + // Custom filters may refer multiple fields. Validate the index id only. + if (filter.meta?.type === 'custom') { + return filter.meta.index === indexPattern.id; + } + return indexPattern.fields.some((field: IFieldType) => field.name === filter.meta.key); } diff --git a/src/plugins/data/common/search/utils.ts b/src/plugins/data/common/search/utils.ts index e87434cd6ca83..647a5fb62e846 100644 --- a/src/plugins/data/common/search/utils.ts +++ b/src/plugins/data/common/search/utils.ts @@ -12,7 +12,7 @@ import type { IKibanaSearchResponse } from './types'; * @returns true if response had an error while executing in ES */ export const isErrorResponse = (response?: IKibanaSearchResponse) => { - return !response || (!response.isRunning && response.isPartial); + return !response || (!response.isRunning && !!response.isPartial); }; /** diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index af7442b5e742f..8dd977e2f3b0c 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1702,7 +1702,7 @@ export interface ISearchStartSearchSource { // Warning: (ae-missing-release-tag) "isErrorResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined; +export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean; // Warning: (ae-forgotten-export) The symbol "SessionsClient" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ISessionsClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 6d68d29d8df69..3d2b1c42f774a 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -412,7 +412,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.numberFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.numberFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -434,7 +434,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.percentFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.percentFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -456,7 +456,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.bytesFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.bytesFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -478,7 +478,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.currencyFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.currencyFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -504,7 +504,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.formattingLocaleText', values: { numeralLanguageLink: - '' + + '' + i18n.translate( 'data.advancedSettings.format.formattingLocale.numeralLanguageLinkText', { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts index d37ae31ea995c..1cc4d2b248b9d 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { getIndices, responseToItemArray, dedupeMatchedItems } from './get_indices'; +import { + getIndices, + getIndicesViaSearch, + responseToItemArray, + dedupeMatchedItems, +} from './get_indices'; import { httpServiceMock } from '../../../../../../core/public/mocks'; import { ResolveIndexResponseItemIndexAttrs, MatchedItem } from '../types'; import { Observable } from 'rxjs'; @@ -34,6 +39,8 @@ export const successfulResolveResponse = { }; const successfulSearchResponse = { + isPartial: false, + isRunning: false, rawResponse: { aggregations: { indices: { @@ -43,6 +50,22 @@ const successfulSearchResponse = { }, }; +const partialSearchResponse = { + isPartial: true, + isRunning: true, + rawResponse: { + hits: { + total: 2, + hits: [], + }, + }, +}; + +const errorSearchResponse = { + isPartial: true, + isRunning: false, +}; + const getIndexTags = () => []; const searchClient = () => new Observable((observer) => { @@ -93,6 +116,22 @@ describe('getIndices', () => { ).toBe(0); }); + it('should work with partial responses', async () => { + const searchClientPartialResponse = () => + new Observable((observer) => { + observer.next(partialSearchResponse); + observer.next(successfulSearchResponse); + observer.complete(); + }) as any; + const result = await getIndices({ + http, + getIndexTags, + pattern: '*:kibana', + searchClient: searchClientPartialResponse, + }); + expect(result.length).toBe(4); + }); + it('response object to item array', () => { const result = { indices: [ @@ -129,12 +168,27 @@ describe('getIndices', () => { }); describe('errors', () => { - it('should handle errors gracefully', async () => { + it('should handle thrown errors gracefully', async () => { http.get.mockImplementationOnce(() => { throw new Error('Test error'); }); const result = await getIndices({ http, getIndexTags, pattern: 'kibana', searchClient }); expect(result.length).toBe(0); }); + + it('getIndicesViaSearch should handle error responses gracefully', async () => { + const searchClientErrorResponse = () => + new Observable((observer) => { + observer.next(errorSearchResponse); + observer.complete(); + }) as any; + const result = await getIndicesViaSearch({ + getIndexTags, + pattern: '*:kibana', + searchClient: searchClientErrorResponse, + showAllIndices: false, + }); + expect(result.length).toBe(0); + }); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts index 94dc6ff37e871..b20d2bcca21da 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts @@ -9,10 +9,15 @@ import { sortBy } from 'lodash'; import { HttpStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { map, scan } from 'rxjs/operators'; +import { map, filter } from 'rxjs/operators'; import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public'; import { MatchedItem, ResolveIndexResponse, ResolveIndexResponseItemIndexAttrs } from '../types'; -import { DataPublicPluginStart, IEsSearchResponse } from '../../../../../data/public'; +import { + DataPublicPluginStart, + IEsSearchResponse, + isErrorResponse, + isCompleteResponse, +} from '../../../../../data/public'; import { MAX_SEARCH_SIZE } from '../constants'; const aliasLabel = i18n.translate('indexPatternManagement.aliasLabel', { defaultMessage: 'Alias' }); @@ -86,8 +91,10 @@ export const getIndicesViaSearch = async ({ }, }, }) - .pipe(map(searchResponseToArray(getIndexTags, showAllIndices))) - .pipe(scan((accumulator = [], value) => accumulator.join(value))) + .pipe( + filter((resp) => isCompleteResponse(resp) || isErrorResponse(resp)), + map(searchResponseToArray(getIndexTags, showAllIndices)) + ) .toPromise() .catch(() => []); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap index 69ea6c481d49b..24ce19710b360 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -11,7 +11,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` helpText={ ; const fieldType = 'number'; const format = { @@ -25,7 +29,20 @@ const formatParams = { const onChange = jest.fn(); const onError = jest.fn(); +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('BytesFormatEditor', () => { + beforeAll(() => { + // Enzyme does not support the new Context API in shallow rendering. + // @see https://github.com/enzymejs/enzyme/issues/2189 + (BytesFormatEditor as React.ComponentType).contextTypes = { + services: () => null, + }; + delete (BytesFormatEditor as Partial).contextType; + }); + it('should have a formatId', () => { expect(BytesFormatEditor.formatId).toEqual('bytes'); }); @@ -38,7 +55,8 @@ describe('BytesFormatEditor', () => { formatParams={formatParams} onChange={onChange} onError={onError} - /> + />, + { context: KibanaReactContext.value } ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap index c73b5e7186547..0d74b583b30d1 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -11,7 +11,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` helpText={ ; + const fieldType = 'number'; const format = { getConverterFor: jest.fn().mockImplementation(() => (input: number) => input * 2), @@ -25,7 +29,20 @@ const formatParams = { const onChange = jest.fn(); const onError = jest.fn(); +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('NumberFormatEditor', () => { + beforeAll(() => { + // Enzyme does not support the new Context API in shallow rendering. + // @see https://github.com/enzymejs/enzyme/issues/2189 + (NumberFormatEditor as React.ComponentType).contextTypes = { + services: () => null, + }; + delete (NumberFormatEditor as Partial).contextType; + }); + it('should have a formatId', () => { expect(NumberFormatEditor.formatId).toEqual('number'); }); @@ -38,7 +55,8 @@ describe('NumberFormatEditor', () => { formatParams={formatParams} onChange={onChange} onError={onError} - /> + />, + { context: KibanaReactContext.value } ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx index 250bbe570a9c4..067bfb914e388 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx @@ -14,13 +14,17 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DefaultFormatEditor, defaultState } from '../default'; import { FormatEditorSamples } from '../../samples'; +import { context as contextType } from '../../../../../../../../kibana_react/public'; export interface NumberFormatEditorParams { pattern: string; } export class NumberFormatEditor extends DefaultFormatEditor { + static contextType = contextType; static formatId = 'number'; + + context!: React.ContextType; state = { ...defaultState, sampleInputs: [10000, 12.345678, -1, -999, 0.52], @@ -43,7 +47,10 @@ export class NumberFormatEditor extends DefaultFormatEditor - + ; + const fieldType = 'number'; const format = { getConverterFor: jest.fn().mockImplementation(() => (input: number) => input * 2), @@ -25,7 +29,20 @@ const formatParams = { const onChange = jest.fn(); const onError = jest.fn(); +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('PercentFormatEditor', () => { + beforeAll(() => { + // Enzyme does not support the new Context API in shallow rendering. + // @see https://github.com/enzymejs/enzyme/issues/2189 + (PercentFormatEditor as React.ComponentType).contextTypes = { + services: () => null, + }; + delete (PercentFormatEditor as Partial).contextType; + }); + it('should have a formatId', () => { expect(PercentFormatEditor.formatId).toEqual('percent'); }); @@ -38,7 +55,8 @@ describe('PercentFormatEditor', () => { formatParams={formatParams} onChange={onChange} onError={onError} - /> + />, + { context: KibanaReactContext.value } ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 33c037c2e84b3..57d05262319f2 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -30,7 +30,7 @@ export interface SaveModalDashboardProps { documentInfo: SaveModalDocumentInfo; objectType: string; onClose: () => void; - onSave: (props: OnSaveProps & { dashboardId: string | null }) => void; + onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); } @@ -48,6 +48,9 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>( documentId || disableDashboardOptions ? null : 'existing' ); + const [isAddToLibrarySelected, setAddToLibrary] = useState( + !initialCopyOnSave || disableDashboardOptions + ); const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( null ); @@ -62,12 +65,13 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { onChange={(option) => { setDashboardOption(option); }} - {...{ copyOnSave, documentId, dashboardOption }} + {...{ copyOnSave, documentId, dashboardOption, setAddToLibrary, isAddToLibrarySelected }} /> ) : null; const onCopyOnSaveChange = (newCopyOnSave: boolean) => { + setAddToLibrary(true); setDashboardOption(null); setCopyOnSave(newCopyOnSave); }; @@ -85,7 +89,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { } } - props.onSave({ ...onSaveProps, dashboardId }); + props.onSave({ ...onSaveProps, dashboardId, addToLibrary: isAddToLibrarySelected }); }; const saveLibraryLabel = @@ -113,7 +117,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { onSave={onModalSave} title={documentInfo.title} showCopyOnSave={documentId ? true : false} - options={dashboardOption === null ? tagOptions : undefined} // Show tags when not adding to dashboard + options={isAddToLibrarySelected ? tagOptions : undefined} // Show tags when not adding to dashboard description={documentInfo.description} showDescription={true} {...{ diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx index 6b41058f9c6c5..dd6fd975f8e07 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx @@ -44,6 +44,7 @@ export function Example({ hasDocumentId: boolean; } & StorybookParams) { const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing'); + const [isAddToLibrarySelected, setAddToLibrary] = useState(false); return ( ); } diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index c2b5eac4dbb83..1ae54040571a2 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -19,6 +19,7 @@ import { EuiIconTip, EuiPanel, EuiSpacer, + EuiCheckbox, } from '@elastic/eui'; import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; @@ -29,24 +30,105 @@ export interface SaveModalDashboardSelectorProps { copyOnSave: boolean; documentId?: string; onSelectDashboard: DashboardPickerProps['onChange']; - + setAddToLibrary: (selected: boolean) => void; + isAddToLibrarySelected: boolean; dashboardOption: 'new' | 'existing' | null; onChange: (dashboardOption: 'new' | 'existing' | null) => void; } export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProps) { - const { documentId, onSelectDashboard, dashboardOption, onChange, copyOnSave } = props; + const { + documentId, + onSelectDashboard, + setAddToLibrary, + isAddToLibrarySelected, + dashboardOption, + onChange, + copyOnSave, + } = props; const isDisabled = !copyOnSave && !!documentId; return ( <> + } + hasChildLabel={false} + > + <> + +
+ <> + onChange('existing')} + disabled={isDisabled} + /> +
+ +
+ + + <> + onChange('new')} + disabled={isDisabled} + /> + + + { + setAddToLibrary(true); + onChange(null); + }} + disabled={isDisabled} + /> +
+
+ - - + setAddToLibrary(event.target.checked)} /> @@ -55,67 +137,13 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp content={ } /> - } - hasChildLabel={false} - > - -
- <> - onChange('existing')} - disabled={isDisabled} - /> -
- -
- - - <> - onChange('new')} - disabled={isDisabled} - /> - - - onChange(null)} - disabled={isDisabled} - /> -
-
+
); diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index e8c3289d4ce41..4f5679a14b0b7 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -93,7 +93,7 @@ export const getTopNavConfig = ( /** * Called when the user clicks "Save" button. */ - async function doSave(saveOptions: SavedObjectSaveOpts) { + async function doSave(saveOptions: SavedObjectSaveOpts & { dashboardId?: string }) { const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave; // vis.title was not bound and it's needed to reflect title into visState stateContainer.transitions.setVis({ @@ -118,7 +118,7 @@ export const getTopNavConfig = ( 'data-test-subj': 'saveVisualizationSuccess', }); - if (originatingApp && saveOptions.returnToOrigin) { + if ((originatingApp && saveOptions.returnToOrigin) || saveOptions.dashboardId) { if (!embeddableId) { const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`; @@ -127,16 +127,26 @@ export const getTopNavConfig = ( setActiveUrl(appPath); } + const app = originatingApp || 'dashboards'; + + let path; + if (saveOptions.dashboardId) { + path = + saveOptions.dashboardId === 'new' ? '#/create' : `#/view/${saveOptions.dashboardId}`; + } + if (newlyCreated && stateTransfer) { - stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { + stateTransfer.navigateToWithEmbeddablePackage(app, { state: { type: VISUALIZE_EMBEDDABLE_TYPE, input: { savedObjectId: id }, embeddableId, }, + path, }); } else { - application.navigateToApp(originatingApp); + // TODO: need the same thing here? + application.navigateToApp(app, { path }); } } else { if (setOriginatingApp && originatingApp && newlyCreated) { @@ -321,7 +331,11 @@ export const getTopNavConfig = ( newDescription, returnToOrigin, dashboardId, - }: OnSaveProps & { returnToOrigin?: boolean } & { dashboardId?: string | null }) => { + addToLibrary, + }: OnSaveProps & { returnToOrigin?: boolean } & { + dashboardId?: string | null; + addToLibrary?: boolean; + }) => { const currentTitle = savedVis.title; savedVis.title = newTitle; embeddableHandler.updateInput({ title: newTitle }); @@ -337,9 +351,12 @@ export const getTopNavConfig = ( isTitleDuplicateConfirmed, onTitleDuplicate, returnToOrigin, + dashboardId: !!dashboardId ? dashboardId : undefined, }; - if (dashboardId) { + // If we're adding to a dashboard and not saving to library, + // we'll want to use a by-value operation + if (dashboardId && !addToLibrary) { const appPath = `${VisualizeConstants.LANDING_PAGE_PATH}`; // Manually insert a new url so the back button will open the saved visualization. @@ -369,6 +386,8 @@ export const getTopNavConfig = ( return { id: true }; } + // We're adding the viz to a library so we need to save it and then + // add to a dashboard if necessary const response = await doSave(saveOptions); // If the save wasn't successful, put the original values back. if (!response.id || response.error) { diff --git a/test/functional/apps/visualize/_add_to_dashboard.ts b/test/functional/apps/visualize/_add_to_dashboard.ts index 1d1bd62988f45..17d628db86d25 100644 --- a/test/functional/apps/visualize/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/_add_to_dashboard.ts @@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); describe('Add to Dashboard', function describeIndexTests() { - it('adding a new metric to a new dashboard', async function () { + it('adding a new metric to a new dashboard by value', async function () { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickMetric(); await PageObjects.visualize.clickNewSearch(); @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timeToVisualize.saveFromModal('My New Vis 1', { addToDashboard: 'new', + saveToLibrary: false, }); await PageObjects.dashboard.waitForRenderComplete(); @@ -43,10 +44,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(1); + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists('My New Vis 1'); + expect(isLinked).to.be(false); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('adding a new metric to a new dashboard by reference', async function () { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + await PageObjects.timeToVisualize.saveFromModal('My Saved New Vis 1', { + addToDashboard: 'new', + saveToLibrary: true, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My Saved New Vis 1' + ); + expect(isLinked).to.be(true); + await PageObjects.timeToVisualize.resetNewDashboard(); }); - it('adding a existing metric to a new dashboard', async function () { + it('adding a existing metric to a new dashboard by value', async function () { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickMetric(); await PageObjects.visualize.clickNewSearch(); @@ -57,6 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Save this new viz to library await PageObjects.timeToVisualize.saveFromModal('My New Vis 1', { addToDashboard: null, + saveToLibrary: true, }); await testSubjects.click('visualizeSaveButton'); @@ -68,6 +99,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timeToVisualize.saveFromModal('My New Vis 1 Copy', { addToDashboard: 'new', saveAsNew: true, + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My New Vis 1 Copy' + ); + expect(isLinked).to.be(false); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('adding a existing metric to a new dashboard by reference', async function () { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + // Save this new viz to library + await PageObjects.timeToVisualize.saveFromModal('Another New Vis 1', { + addToDashboard: null, + saveToLibrary: true, + }); + + await testSubjects.click('visualizeSaveButton'); + + // All the options should be disabled + await PageObjects.timeToVisualize.ensureDashboardOptionsAreDisabled(); + + // Save a new copy of this viz to a new dashboard + await PageObjects.timeToVisualize.saveFromModal('Another New Vis 1 Copy', { + addToDashboard: 'new', + saveAsNew: true, + saveToLibrary: true, }); await PageObjects.dashboard.waitForRenderComplete(); @@ -75,10 +146,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(1); + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'Another New Vis 1 Copy' + ); + expect(isLinked).to.be(true); + await PageObjects.timeToVisualize.resetNewDashboard(); }); - it('adding a new metric to an existing dashboard', async function () { + it('adding a new metric to an existing dashboard by value', async function () { + await PageObjects.common.navigateToApp('dashboard'); + + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.addVisualizations(['Visualization AreaChart']); + await PageObjects.dashboard.saveDashboard('My Excellent Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Excellent Dashboard', 1); + + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + await PageObjects.timeToVisualize.saveFromModal('My New Vis 2', { + addToDashboard: 'existing', + dashboardId: 'My Excellent Dashboard', + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists('My New Vis 2'); + expect(isLinked).to.be(false); + }); + + it('adding a new metric to an existing dashboard by reference', async function () { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); @@ -94,18 +201,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('visualizeSaveButton'); - await PageObjects.timeToVisualize.saveFromModal('My New Vis 2', { + await PageObjects.timeToVisualize.saveFromModal('My Saved New Vis 2', { addToDashboard: 'existing', dashboardId: 'My Wonderful Dashboard', + saveToLibrary: true, }); await PageObjects.dashboard.waitForRenderComplete(); await dashboardExpect.metricValuesExist(['14,004']); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My Saved New Vis 2' + ); + expect(isLinked).to.be(true); }); - it('adding a existing metric to an existing dashboard', async function () { + it('adding a existing metric to an existing dashboard by value', async function () { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); @@ -124,6 +237,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Save this new viz to library await PageObjects.timeToVisualize.saveFromModal('My New Vis 2', { addToDashboard: null, + saveToLibrary: true, }); await testSubjects.click('visualizeSaveButton'); @@ -136,12 +250,64 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { addToDashboard: 'existing', dashboardId: 'My Very Cool Dashboard', saveAsNew: true, + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My New Vis 2 Copy' + ); + expect(isLinked).to.be(false); + }); + + it('adding a existing metric to an existing dashboard by reference', async function () { + await PageObjects.common.navigateToApp('dashboard'); + + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.addVisualizations(['Visualization AreaChart']); + await PageObjects.dashboard.saveDashboard('My Very Neat Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Very Neat Dashboard', 1); + + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + // Save this new viz to library + await PageObjects.timeToVisualize.saveFromModal('Neat Saved Vis 2', { + addToDashboard: null, + saveToLibrary: true, + }); + + await testSubjects.click('visualizeSaveButton'); + + // All the options should be disabled + await PageObjects.timeToVisualize.ensureDashboardOptionsAreDisabled(); + + // Save a new copy of this viz to an existing dashboard + await PageObjects.timeToVisualize.saveFromModal('Neat Saved Vis 2 Copy', { + addToDashboard: 'existing', + dashboardId: 'My Very Neat Dashboard', + saveAsNew: true, + saveToLibrary: true, }); await PageObjects.dashboard.waitForRenderComplete(); await dashboardExpect.metricValuesExist(['14,004']); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'Neat Saved Vis 2 Copy' + ); + expect(isLinked).to.be(true); }); }); } diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 560f73cbcdbd8..458b4dd3e60a1 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; interface SaveModalArgs { addToDashboard?: 'new' | 'existing' | null; + saveToLibrary?: boolean; dashboardId?: string; saveAsNew?: boolean; redirectToOrigin?: boolean; @@ -35,7 +36,9 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); await dashboardSelector.findByCssSelector(`input[id="new-dashboard-option"]:disabled`); await dashboardSelector.findByCssSelector(`input[id="existing-dashboard-option"]:disabled`); - await dashboardSelector.findByCssSelector(`input[id="add-to-library-option"]:disabled`); + + const librarySelector = await testSubjects.find('add-to-library-checkbox'); + await librarySelector.findByCssSelector(`input[id="add-to-library-checkbox"]:disabled`); } public async resetNewDashboard() { @@ -46,7 +49,13 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP public async setSaveModalValues( vizName: string, - { saveAsNew, redirectToOrigin, addToDashboard, dashboardId }: SaveModalArgs = {} + { + saveAsNew, + redirectToOrigin, + addToDashboard, + dashboardId, + saveToLibrary, + }: SaveModalArgs = {} ) { await testSubjects.setValue('savedObjectTitle', vizName); @@ -57,13 +66,6 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP await testSubjects.setEuiSwitch('saveAsNewCheckbox', state); } - const hasRedirectToOrigin = await testSubjects.exists('returnToOriginModeSwitch'); - if (hasRedirectToOrigin && redirectToOrigin !== undefined) { - const state = redirectToOrigin ? 'check' : 'uncheck'; - log.debug('redirect to origin checkbox exists. Setting its state to', state); - await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state); - } - const hasDashboardSelector = await testSubjects.exists('add-to-dashboard-options'); if (hasDashboardSelector && addToDashboard !== undefined) { let option: DashboardPickerOption = 'add-to-library-option'; @@ -80,6 +82,40 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP await find.clickByButtonText(dashboardId); } } + + const hasSaveToLibrary = await testSubjects.exists('add-to-library-checkbox'); + if (hasSaveToLibrary && saveToLibrary !== undefined) { + const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox'); + const isChecked = await libraryCheckbox.isSelected(); + const needsClick = isChecked !== saveToLibrary; + const state = saveToLibrary ? 'check' : 'uncheck'; + + log.debug('save to library checkbox exists. Setting its state to', state); + if (needsClick) { + const selector = await testSubjects.find('add-to-library-checkbox'); + const label = await selector.findByCssSelector(`label[for="add-to-library-checkbox"]`); + await label.click(); + } + } + + const hasRedirectToOrigin = await testSubjects.exists('returnToOriginModeSwitch'); + if (hasRedirectToOrigin && redirectToOrigin !== undefined) { + const state = redirectToOrigin ? 'check' : 'uncheck'; + log.debug('redirect to origin checkbox exists. Setting its state to', state); + await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state); + } + } + + public async libraryNotificationExists(panelTitle: string) { + log.debug('searching for library modal on panel:', panelTitle); + const panel = await testSubjects.find( + `embeddablePanelHeading-${panelTitle.replace(/ /g, '')}` + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + panel + ); + return libraryActionExists; } public async saveFromModal( diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 64663f8330b24..676ce1d27d2fc 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -571,6 +571,132 @@ describe('7.11.2', () => { } as SavedObjectUnsanitizedDoc; expect(isAnyActionSupportIncidents(doc)).toBe(false); }); + + test('it does not transforms alerts when the right structure connectors is already applied', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + params: { + level: 'info', + message: 'log message', + }, + id: '99257478-e591-4560-b264-441bdd4fe1d9', + }, + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + }, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual(alert); + }); + + test('if incident attribute is an empty object, copy back the related attributes from subActionParams back to incident', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + params: { + level: 'info', + message: 'log message', + }, + id: '99257478-e591-4560-b264-441bdd4fe1d9', + }, + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + incident: {}, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + alert.attributes.actions![0], + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + }, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }, + }); + }); + + test('custom action does not get migrated/loss', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.mike', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + incident: {}, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual(alert); + }); }); function getUpdatedAt(): string { diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index f2f956a0a2b26..729290498561f 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -10,6 +10,7 @@ import { SavedObjectUnsanitizedDoc, SavedObjectMigrationFn, SavedObjectMigrationContext, + SavedObjectAttributes, } from '../../../../../src/core/server'; import { RawAlert, RawAlertAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; @@ -180,113 +181,147 @@ function initializeExecutionStatus( }; } +function isEmptyObject(obj: {}) { + for (const attr in obj) { + if (Object.prototype.hasOwnProperty.call(obj, attr)) { + return false; + } + } + return true; +} + function restructureConnectorsThatSupportIncident( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc { const { actions } = doc.attributes; const newActions = actions.reduce((acc, action) => { - if (action.params.subAction !== 'pushToService') { - return [...acc, action]; - } - - if (action.actionTypeId === '.servicenow') { - const { title, comments, comment, description, severity, urgency, impact } = action.params - .subActionParams as { - title: string; - description?: string; - severity?: string; - urgency?: string; - impact?: string; - comment?: string; - comments?: Array<{ commentId: string; comment: string }>; - }; - return [ - ...acc, - { - ...action, - params: { - subAction: 'pushToService', - subActionParams: { - incident: { - short_description: title, - description, - severity, - urgency, - impact, + if ( + ['.servicenow', '.jira', '.resilient'].includes(action.actionTypeId) && + action.params.subAction === 'pushToService' + ) { + // Future developer, we needed to do that because when we created this migration + // we forget to think about user already using 7.11.0 and having an incident attribute build the right way + // IMPORTANT -> if you change this code please do the same inside of this file + // x-pack/plugins/alerting/server/saved_objects/migrations.ts + const subActionParamsIncident = + (action.params?.subActionParams as SavedObjectAttributes)?.incident ?? null; + if (subActionParamsIncident != null && !isEmptyObject(subActionParamsIncident)) { + return [...acc, action]; + } + if (action.actionTypeId === '.servicenow') { + const { + title, + comments, + comment, + description, + severity, + urgency, + impact, + short_description: shortDescription, + } = action.params.subActionParams as { + title: string; + description?: string; + severity?: string; + urgency?: string; + impact?: string; + comment?: string; + comments?: Array<{ commentId: string; comment: string }>; + short_description?: string; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: shortDescription ?? title, + description, + severity, + urgency, + impact, + }, + comments: [ + ...(comments ?? []), + ...(comment != null ? [{ commentId: '1', comment }] : []), + ], }, - comments: [ - ...(comments ?? []), - ...(comment != null ? [{ commentId: '1', comment }] : []), - ], }, }, - }, - ] as RawAlertAction[]; - } - - if (action.actionTypeId === '.jira') { - const { title, comments, description, issueType, priority, labels, parent } = action.params - .subActionParams as { - title: string; - description: string; - issueType: string; - priority?: string; - labels?: string[]; - parent?: string; - comments?: unknown[]; - }; - return [ - ...acc, - { - ...action, - params: { - subAction: 'pushToService', - subActionParams: { - incident: { - summary: title, - description, - issueType, - priority, - labels, - parent, + ] as RawAlertAction[]; + } else if (action.actionTypeId === '.jira') { + const { + title, + comments, + description, + issueType, + priority, + labels, + parent, + summary, + } = action.params.subActionParams as { + title: string; + description: string; + issueType: string; + priority?: string; + labels?: string[]; + parent?: string; + comments?: unknown[]; + summary?: string; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: summary ?? title, + description, + issueType, + priority, + labels, + parent, + }, + comments, }, - comments, }, }, - }, - ] as RawAlertAction[]; - } - - if (action.actionTypeId === '.resilient') { - const { title, comments, description, incidentTypes, severityCode } = action.params - .subActionParams as { - title: string; - description: string; - incidentTypes?: number[]; - severityCode?: number; - comments?: unknown[]; - }; - return [ - ...acc, - { - ...action, - params: { - subAction: 'pushToService', - subActionParams: { - incident: { - name: title, - description, - incidentTypes, - severityCode, + ] as RawAlertAction[]; + } else if (action.actionTypeId === '.resilient') { + const { title, comments, description, incidentTypes, severityCode, name } = action.params + .subActionParams as { + title: string; + description: string; + incidentTypes?: number[]; + severityCode?: number; + comments?: unknown[]; + name?: string; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + name: name ?? title, + description, + incidentTypes, + severityCode, + }, + comments, }, - comments, }, }, - }, - ] as RawAlertAction[]; + ] as RawAlertAction[]; + } } - return acc; + return [...acc, action]; }, [] as RawAlertAction[]); return { diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index e5b9242f29ac3..bf67d176ac375 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -171,6 +171,34 @@ describe('case connector', () => { }, }, }, + { + test: 'servicenow-sir', + params: { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'servicenow-sir', + name: 'Servicenow SIR', + type: '.servicenow-sir', + fields: { + destIp: true, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + category: 'ddos', + subcategory: '15', + priority: '1', + }, + }, + settings: { + syncAlerts: true, + }, + }, + }, + }, { test: 'none', params: { @@ -474,7 +502,7 @@ describe('case connector', () => { }); }); - it('succeeds when servicenow fields are valid', () => { + it('succeeds when servicenow ITSM fields are valid', () => { const params: Record = { subAction: 'update', subActionParams: { @@ -508,6 +536,42 @@ describe('case connector', () => { }); }); + it('succeeds when servicenow SIR fields are valid', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'servicenow-sir', + name: 'Servicenow SIR', + type: '.servicenow-sir', + fields: { + destIp: true, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + category: 'ddos', + subcategory: '15', + priority: '1', + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + description: null, + tags: null, + title: null, + status: null, + settings: null, + ...(params.subActionParams as Record), + }, + }); + }); + it('set fields to null if they are missing', () => { const params: Record = { subAction: 'update', diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index 3e46d0c0525e6..d804c27da8e17 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -56,7 +56,7 @@ const ResilientFieldsSchema = schema.object({ severityCode: schema.nullable(schema.string()), }); -const ServiceNowFieldsSchema = schema.object({ +const ServiceNowITSMFieldsSchema = schema.object({ impact: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), urgency: schema.nullable(schema.string()), @@ -64,11 +64,22 @@ const ServiceNowFieldsSchema = schema.object({ subcategory: schema.nullable(schema.string()), }); +const ServiceNowSIRFieldsSchema = schema.object({ + destIp: schema.nullable(schema.boolean()), + sourceIp: schema.nullable(schema.boolean()), + malwareHash: schema.nullable(schema.boolean()), + malwareUrl: schema.nullable(schema.boolean()), + priority: schema.nullable(schema.string()), + category: schema.nullable(schema.string()), + subcategory: schema.nullable(schema.string()), +}); + const NoneFieldsSchema = schema.nullable(schema.object({})); const ReducedConnectorFieldsSchema: { [x: string]: any } = { '.jira': JiraFieldsSchema, '.resilient': ResilientFieldsSchema, + '.servicenow-sir': ServiceNowSIRFieldsSchema, }; export const ConnectorProps = { @@ -78,6 +89,7 @@ export const ConnectorProps = { schema.literal('.servicenow'), schema.literal('.jira'), schema.literal('.resilient'), + schema.literal('.servicenow-sir'), schema.literal('.none'), ]), // Chain of conditional schemes @@ -92,7 +104,7 @@ export const ConnectorProps = { schema.conditional( schema.siblingRef('type'), '.servicenow', - ServiceNowFieldsSchema, + ServiceNowITSMFieldsSchema, NoneFieldsSchema ) ), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx index a992c1a274063..b027ec63ac256 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -54,7 +54,7 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ {agentPolicy.name || agentPolicy.id} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx index abe4eb0fa5916..03bdc2ceb9579 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx @@ -44,7 +44,15 @@ interface Props { /** * Validation to be applied to every text item */ - textValidation?: ValidationFunc; + textValidations?: Array>; + /** + * Serializer to be applied to every text item + */ + textSerializer?: (v: string) => O; + /** + * Deserializer to be applied to every text item + */ + textDeserializer?: (v: unknown) => string; } const i18nTexts = { @@ -63,7 +71,9 @@ function DragAndDropTextListComponent({ onAdd, onRemove, addLabel, - textValidation, + textValidations, + textDeserializer, + textSerializer, }: Props): JSX.Element { const [droppableId] = useState(() => uuid.v4()); const [firstItemId] = useState(() => uuid.v4()); @@ -133,9 +143,11 @@ function DragAndDropTextListComponent({ path={item.path} config={{ - validations: textValidation - ? [{ validator: textValidation }] + validations: textValidations + ? textValidations.map((validator) => ({ validator })) : undefined, + deserializer: textDeserializer, + serializer: textSerializer, }} readDefaultValueOnForm={!item.isNew} > diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 6652ad277cc26..3864581317e38 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -22,7 +22,7 @@ import { import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { EDITOR_PX_HEIGHT, from } from './shared'; +import { EDITOR_PX_HEIGHT, from, to, isJSONStringValidator } from './shared'; const { emptyField } = fieldValidators; @@ -34,6 +34,8 @@ const getFieldsConfig = (esDocUrl: string): Record => { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', { defaultMessage: 'Pattern', }), + deserializer: to.escapeBackslashes, + serializer: from.unescapeBackslashes, helpText: ( => { ) ), }, + { + validator: isJSONStringValidator, + }, ], }, /* Optional field config */ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx index f15441ea1f92b..ae2d341c58c30 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { flow } from 'lodash'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; @@ -22,7 +23,7 @@ import { XJsonEditor, DragAndDropTextList } from '../field_components'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared'; +import { FieldsConfig, to, from, EDITOR_PX_HEIGHT, isJSONStringValidator } from './shared'; const { isJsonField, emptyField } = fieldValidators; @@ -46,7 +47,10 @@ const patternsValidation: ValidationFunc = ({ value, f } }; -const patternValidation = emptyField(valueRequiredMessage); +const patternValidations: Array> = [ + emptyField(valueRequiredMessage), + isJSONStringValidator, +]; const fieldsConfig: FieldsConfig = { /* Required field configs */ @@ -54,6 +58,8 @@ const fieldsConfig: FieldsConfig = { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.patternsFieldLabel', { defaultMessage: 'Patterns', }), + deserializer: flow(String, to.escapeBackslashes), + serializer: from.unescapeBackslashes, helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.patternsHelpText', { defaultMessage: 'Grok expressions used to match and extract named capture groups. Uses the first matching expression.', @@ -133,7 +139,9 @@ export const Grok: FunctionComponent = () => { onAdd={addItem} onRemove={removeItem} addLabel={i18nTexts.addPatternLabel} - textValidation={patternValidation} + textValidations={patternValidations} + textDeserializer={fieldsConfig.patterns?.deserializer} + textSerializer={fieldsConfig.patterns?.serializer} /> ); }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx index edfa59ea80281..11d06f3cca6fb 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { flow } from 'lodash'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; @@ -12,7 +13,7 @@ import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../.. import { TextEditor } from '../field_components'; -import { EDITOR_PX_HEIGHT, FieldsConfig } from './shared'; +import { EDITOR_PX_HEIGHT, FieldsConfig, from, to, isJSONStringValidator } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { TargetField } from './common_fields/target_field'; @@ -26,7 +27,8 @@ const fieldsConfig: FieldsConfig = { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldLabel', { defaultMessage: 'Pattern', }), - deserializer: String, + deserializer: flow(String, to.escapeBackslashes), + serializer: from.unescapeBackslashes, helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldHelpText', { defaultMessage: 'Regular expression used to match substrings in the field.', }), @@ -38,6 +40,9 @@ const fieldsConfig: FieldsConfig = { }) ), }, + { + validator: isJSONStringValidator, + }, ], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.test.ts new file mode 100644 index 0000000000000..4b01f22a9383d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { from, to } from './shared'; + +describe('shared', () => { + describe('deserialization helpers', () => { + // This is the text that will be passed to the text input + test('to.escapeBackslashes', () => { + // this input loaded from the server + const input1 = 'my\ttab'; + expect(to.escapeBackslashes(input1)).toBe('my\\ttab'); + + // this input loaded from the server + const input2 = 'my\\ttab'; + expect(to.escapeBackslashes(input2)).toBe('my\\\\ttab'); + + // this input loaded from the server + const input3 = '\t\n\rOK'; + expect(to.escapeBackslashes(input3)).toBe('\\t\\n\\rOK'); + + const input4 = `%{clientip} %{ident} %{auth} [%{@timestamp}] \"%{verb} %{request} HTTP/%{httpversion}\" %{status} %{size}`; + expect(to.escapeBackslashes(input4)).toBe( + '%{clientip} %{ident} %{auth} [%{@timestamp}] \\"%{verb} %{request} HTTP/%{httpversion}\\" %{status} %{size}' + ); + }); + }); + + describe('serialization helpers', () => { + test('from.unescapeBackslashes', () => { + // user typed in "my\ttab" + const input1 = 'my\\ttab'; + expect(from.unescapeBackslashes(input1)).toBe('my\ttab'); + + // user typed in "my\\tab" + const input2 = 'my\\\\ttab'; + expect(from.unescapeBackslashes(input2)).toBe('my\\ttab'); + + // user typed in "\t\n\rOK" + const input3 = '\\t\\n\\rOK'; + expect(from.unescapeBackslashes(input3)).toBe('\t\n\rOK'); + + const input5 = `%{clientip} %{ident} %{auth} [%{@timestamp}] \\"%{verb} %{request} HTTP/%{httpversion}\\" %{status} %{size}`; + expect(from.unescapeBackslashes(input5)).toBe( + `%{clientip} %{ident} %{auth} [%{@timestamp}] \"%{verb} %{request} HTTP/%{httpversion}\" %{status} %{size}` + ); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts index 399da3c05c783..bafba412c767f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; import * as rt from 'io-ts'; +import { i18n } from '@kbn/i18n'; import { isRight } from 'fp-ts/lib/Either'; -import { FieldConfig } from '../../../../../../shared_imports'; +import { FieldConfig, ValidationFunc } from '../../../../../../shared_imports'; export const arrayOfStrings = rt.array(rt.string); @@ -36,6 +37,17 @@ export const to = { arrayOfStrings: (v: unknown): string[] => isArrayOfStrings(v) ? v : typeof v === 'string' && v.length ? [v] : [], jsonString: (v: unknown) => (v ? JSON.stringify(v, null, 2) : '{}'), + /** + * Useful when deserializing strings that will be rendered inside of text areas or text inputs. We want + * a string like: "my\ttab" to render the same, not to render as "mytab". + */ + escapeBackslashes: (v: unknown) => { + if (typeof v === 'string') { + const s = JSON.stringify(v); + return s.slice(1, s.length - 1); + } + return v; + }, }; /** @@ -69,6 +81,41 @@ export const from = { optionalArrayOfStrings: (v: string[]) => (v.length ? v : undefined), undefinedIfValue: (value: unknown) => (v: boolean) => (v === value ? undefined : v), emptyStringToUndefined: (v: unknown) => (v === '' ? undefined : v), + /** + * Useful when serializing user input from a