diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index e1cbac0528b1f9..8ad02b7162b6ab 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -34,7 +34,7 @@ stage("Kibana Pipeline") { if (!IS_XPACK) { kibanaPipeline.buildOss() if (CI_GROUP == '1') { - runbld "./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh" + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") } } else { kibanaPipeline.buildXpack() @@ -62,18 +62,18 @@ stage("Kibana Pipeline") { def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }) + return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }) + return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) } else { return kibanaPipeline.getOssCiGroupWorker(ciGroup) } } if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }) + return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }) } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }) + return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }) } else { return kibanaPipeline.getXpackCiGroupWorker(ciGroup) } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 94296d076189bf..0f1136fd5334ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,11 +28,6 @@ # Canvas /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas -# Code -/x-pack/legacy/plugins/code/ @teams/code -/x-pack/test/functional/apps/code/ @teams/code -/x-pack/test/api_integration/apis/code/ @teams/code - # Logs & Metrics UI /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/legacy/plugins/integrations_manager/ @elastic/epm diff --git a/Jenkinsfile b/Jenkinsfile index 8d8579736f6392..c002832d4d51a3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -24,9 +24,9 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }), - 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { runbld './test/scripts/jenkins_accessibility.sh' }), - 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }), + 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }), + 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { runbld('./test/scripts/jenkins_accessibility.sh', 'Execute kibana-accessibility') }), + 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), ]), 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), @@ -39,9 +39,9 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }), - 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { runbld './test/scripts/jenkins_xpack_accessibility.sh' }), - 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }), + 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }), + 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { runbld('./test/scripts/jenkins_xpack_accessibility.sh', 'Execute xpack-accessibility') }), + 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }), ]), ]) } diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 5fd3ef5e8ff4bf..461d51a3e76e30 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -120,6 +120,7 @@ You should prefer modern language features in a lot of cases, e.g.: * Prefer arrow function over storing `this` (no `const self = this;`) * Prefer template strings over string concatenation * Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` +* Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) ### Avoid mutability and state diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index caff7f5b1fdc6a..38fceeb47d6fd2 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -9,11 +9,12 @@ for displayed decimal values. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. + [float] [[settings-read-only-access]] === [xpack]#Read only access# -When you have insufficient privileges to edit advanced settings, the following -indicator in Kibana will be displayed. The buttons to edit settings won't be visible. +When you have insufficient privileges to edit advanced settings, the following +indicator in Kibana will be displayed. The buttons to edit settings won't be visible. For more information on granting access to Kibana see <>. [role="screenshot"] @@ -25,9 +26,9 @@ image::images/settings-read-only-badge.png[Example of Advanced Settings Manageme WARNING: Modifying a setting can affect {kib} performance and cause problems that are -difficult to diagnose. Setting a property value to a blank field reverts +difficult to diagnose. Setting a property value to a blank field reverts to the default behavior, which might not be -compatible with other configuration settings. Deleting a custom setting +compatible with other configuration settings. Deleting a custom setting removes it from {kib} permanently. @@ -44,7 +45,7 @@ removes it from {kib} permanently. adapt to the interval between measurements. Keys are http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. `dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser. `dateNanosFormat`:: The format to use for displaying https://momentjs.com/docs/#/displaying/format/[pretty formatted dates] of {ref}/date_nanos.html[Elasticsearch date_nanos type]. -`defaultIndex`:: The index to access if no index is set. The default is `null`. +`defaultIndex`:: The index to access if no index is set. The default is `null`. `fields:popularLimit`:: The top N most popular fields to show. `filterEditor:suggestValues`:: Set this property to `false` to prevent the filter editor from suggesting values for fields. `filters:pinnedByDefault`:: Set this property to `true` to make filters have a global state (be pinned) by default. @@ -59,46 +60,46 @@ mentioned use "\_default_". `histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values when necessary. `history:limit`:: In fields that have history, such as query inputs, show this many recent values. -`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, +`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, look for this many recent matching patterns from which to query the field mapping. `indexPattern:placeholder`:: The default placeholder value to use in Management > Index Patterns > Create Index Pattern. -`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields +`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single -data source can return. This might arise when the user selects a +data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character -in a query clause. Only applies when experimental query features are -enabled in the query bar. To disallow leading wildcards in Lucene queries, +`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character +in a query clause. Only applies when experimental query features are +enabled in the query bar. To disallow leading wildcards in Lucene queries, use `query:queryString:options`. `query:queryString:options`:: Options for the Lucene query string parser. Only used when "Query language" is set to Lucene. -`savedObjects:listingLimit`:: The number of objects to fetch for lists of saved objects. +`savedObjects:listingLimit`:: The number of objects to fetch for lists of saved objects. The default value is 1000. Do not set above 10000. -`savedObjects:perPage`:: The number of objects to show on each page of the +`savedObjects:perPage`:: The number of objects to show on each page of the list of saved objects. The default is 5. `search:queryLanguage`:: The query language to use in the query bar. -Choices are <>, a language built specifically for {kib}, and the <>, a language built specifically for {kib}, and the <>. -`shortDots:enable`:: Set this property to `true` to shorten long +`shortDots:enable`:: Set this property to `true` to shorten long field names in visualizations. For example, show `f.b.baz` instead of `foo.bar.baz`. `sort:options`:: Options for the Elasticsearch {ref}/search-request-body.html#request-body-search-sort[sort] parameter. -`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the -URL, which can lead to problems when there is a lot of state information, -and the URL gets very long. -Enabling this setting stores part of the URL in your browser session to keep the +`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the +URL, which can lead to problems when there is a lot of state information, +and the URL gets very long. +Enabling this setting stores part of the URL in your browser session to keep the URL short. `theme:darkMode`:: Set to `true` to enable a dark mode for the {kib} UI. You must refresh the page to apply the setting. -`timepicker:quickRanges`:: The list of ranges to show in the Quick section of -the time filter. This should be an array of objects, with each object containing -`from`, `to` (see {ref}/common-options.html#date-math[accepted formats]), +`timepicker:quickRanges`:: The list of ranges to show in the Quick section of +the time filter. This should be an array of objects, with each object containing +`from`, `to` (see {ref}/common-options.html#date-math[accepted formats]), and `display` (the title to be displayed). `timepicker:refreshIntervalDefaults`:: The default refresh interval for the time filter. Example: `{ "display": "15 seconds", "pause": true, "value": 15000 }`. `timepicker:timeDefaults`:: The default selection in the time filter. `truncate:maxHeight`:: The maximum height that a cell occupies in a table. Set to 0 to disable truncation. -`xPack:defaultAdminEmail`:: Email address for X-Pack admin operations, such as +`xPack:defaultAdminEmail`:: Email address for X-Pack admin operations, such as cluster alert notifications from Monitoring. @@ -107,7 +108,7 @@ cluster alert notifications from Monitoring. === Accessibility settings [horizontal] -`accessibility:disableAnimations`:: Turns off all unnecessary animations in the +`accessibility:disableAnimations`:: Turns off all unnecessary animations in the {kib} UI. Refresh the page to apply the changes. [float] @@ -124,21 +125,21 @@ cluster alert notifications from Monitoring. [horizontal] `context:defaultSize`:: The number of surrounding entries to display in the context view. The default value is 5. `context:step`:: The number by which to increment or decrement the context size. The default value is 5. -`context:tieBreakerFields`:: A comma-separated list of fields to use -for breaking a tie between documents that have the same timestamp value. The first +`context:tieBreakerFields`:: A comma-separated list of fields to use +for breaking a tie between documents that have the same timestamp value. The first field that is present and sortable in the current index pattern is used. `defaultColumns`:: The columns that appear by default on the Discover page. -The default is `_source`. -`discover:aggs:terms:size`:: The number terms that are visualized when clicking +The default is `_source`. +`discover:aggs:terms:size`:: The number terms that are visualized when clicking the Visualize button in the field drop down. The default is `20`. `discover:sampleSize`:: The number of rows to show in the Discover table. `discover:sort:defaultOrder`:: The default sort direction for time-based index patterns. -`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. +`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. This setting does not have an effect when loading a saved search. `doc_table:hideTimeColumn`:: Hides the "Time" column in Discover and in all saved searches on dashboards. -`doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. +`doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. Highlighting slows requests when -working on big documents. +working on big documents. @@ -150,14 +151,14 @@ working on big documents. [horizontal] `notifications:banner`:: A custom banner intended for temporary notices to all users. Supports https://help.github.com/en/articles/basic-writing-and-formatting-syntax[Markdown]. -`notifications:lifetime:banner`:: The duration, in milliseconds, for banner -notification displays. The default value is 3000000. Set this field to `Infinity` +`notifications:lifetime:banner`:: The duration, in milliseconds, for banner +notification displays. The default value is 3000000. Set this field to `Infinity` to disable banner notifications. -`notifications:lifetime:error`:: The duration, in milliseconds, for error +`notifications:lifetime:error`:: The duration, in milliseconds, for error notification displays. The default value is 300000. Set this field to `Infinity` to disable error notifications. -`notifications:lifetime:info`:: The duration, in milliseconds, for information notification displays. +`notifications:lifetime:info`:: The duration, in milliseconds, for information notification displays. The default value is 5000. Set this field to `Infinity` to disable information notifications. -`notifications:lifetime:warning`:: The duration, in milliseconds, for warning notification +`notifications:lifetime:warning`:: The duration, in milliseconds, for warning notification displays. The default value is 10000. Set this field to `Infinity` to disable warning notifications. @@ -175,8 +176,8 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa === Rollup settings [horizontal] -`rollups:enableIndexPatterns`:: Enables the creation of index patterns that -capture rollup indices, which in turn enables visualizations based on rollup data. +`rollups:enableIndexPatterns`:: Enables the creation of index patterns that +capture rollup indices, which in turn enables visualizations based on rollup data. Refresh the page to apply the changes. @@ -188,22 +189,22 @@ Refresh the page to apply the changes. `courier:batchSearches`:: When disabled, dashboard panels will load individually, and search requests will terminate when users navigate away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, and searches will not terminate. -`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference] +`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference] to use when `courier:setRequestPreference` is set to "custom". -`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. +`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. Useful when dashboards consist of visualizations from multiple index patterns. -`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] -setting used for `_msearch` requests sent by {kib}. Set to 0 to disable this +`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] +setting used for `_msearch` requests sent by {kib}. Set to 0 to disable this config and use the {es} default. `courier:setRequestPreference`:: Enables you to set which shards handle your search requests. -* *Session ID:* Restricts operations to execute all search requests on the same shards. +* *Session ID:* Restricts operations to execute all search requests on the same shards. This has the benefit of reusing shard caches across requests. -* *Custom:* Allows you to define your own preference. Use `courier:customRequestPreference` +* *Custom:* Allows you to define your own preference. Use `courier:customRequestPreference` to customize your preference value. -* *None:* Do not set a preference. This might provide better performance -because requests can be spread across all shard copies. However, results might +* *None:* Do not set a preference. This might provide better performance +because requests can be spread across all shard copies. However, results might be inconsistent because different shards might be in different refresh states. -`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. +`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. Searching through frozen indices might increase the search time. This setting is off by default. Users must opt-in to include frozen indices. @@ -212,8 +213,8 @@ might increase the search time. This setting is off by default. Users must opt-i === SIEM settings [horizontal] -`siem:defaultAnomalyScore`:: The threshold above which anomalies are displayed in the SIEM app. -`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. +`siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. +`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. `siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds. `siem:timeDefaults`:: The default period of time in the SIEM time filter. @@ -226,16 +227,16 @@ might increase the search time. This setting is off by default. Users must opt-i `timelion:default_rows`:: The default number of rows to use on a Timelion sheet. `timelion:es.default_index`:: The default index when using the `.es()` query. `timelion:es.timefield`:: The default field containing a timestamp when using the `.es()` query. -`timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host -in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be +`timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host +in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be selected from a whitelist configured in the `kibana.yml` under `timelion.graphiteUrls`. `timelion:max_buckets`:: The maximum number of buckets a single data source can return. This value is used for calculating automatic intervals in visualizations. `timelion:min_interval`:: The smallest interval to calculate when using "auto". `timelion:quandl.key`:: [experimental] Used with quandl queries, this is your API key from https://www.quandl.com/[www.quandl.com]. -`timelion:showTutorial`:: Shows the Timelion tutorial +`timelion:showTutorial`:: Shows the Timelion tutorial to users when they first open the Timelion app. -`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, +`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, this is the number of buckets to try to represent. @@ -246,18 +247,18 @@ this is the number of buckets to try to represent. [horizontal] `visualization:colorMapping`:: Maps values to specified colors in visualizations. -`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed -when highlighting another element of the chart. The lower this number, the more +`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed +when highlighting another element of the chart. The lower this number, the more the highlighted element stands out. This must be a number between 0 and 1. -`visualization:loadingDelay`:: The time to wait before dimming visualizations +`visualization:loadingDelay`:: The time to wait before dimming visualizations during a query. -`visualization:regionmap:showWarnings`:: Shows +`visualization:regionmap:showWarnings`:: Shows a warning in a region map when terms cannot be joined to a shape. `visualization:tileMap:WMSdefaults`:: The default properties for the WMS map server support in the coordinate map. `visualization:tileMap:maxPrecision`:: The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, -and 12 is the maximum. See this +and 12 is the maximum. See this {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[explanation of cell dimensions]. -`visualize:enableLabs`:: Enables users to create, view, and edit experimental visualizations. +`visualize:enableLabs`:: Enables users to create, view, and edit experimental visualizations. If disabled, only visualizations that are considered production-ready are available to the user. @@ -265,6 +266,5 @@ If disabled, only visualizations that are considered production-ready are availa [[kibana-telemetry-settings]] === Usage data settings -Helps improve the Elastic Stack by providing usage statistics for +Helps improve the Elastic Stack by providing usage statistics for basic features. This data will not be shared outside of Elastic. - diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 0f17ffcf26930e..f4434ea7a09f46 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -32,7 +32,8 @@ strongly recommend that you keep the default CSP rules that ship with Kibana. `csp.strict:`:: *Default: `true`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable -support for older, less safe browsers like Internet Explorer. +support for older, less safe browsers like Internet Explorer. +See <> for more information. `csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after loading Kibana to any browser that does not enforce even rudimentary CSP rules, @@ -319,6 +320,18 @@ supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2 setting this to `true` enables unauthenticated users to access the Kibana server status API and status page. +`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, +users are able to change the telemetry setting at a later time in +<>. If `false`, +{kib} looks at the value of `telemetry.optIn` to determine whether to send +telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` +cannot be `false` at the same time. + +`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. + If `false`, collection of telemetry data is disabled. + To enable telemetry and prevent users from disabling it, + set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. + `vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. `xpack.license_management.enabled`:: *Default: true* Set this value to false to diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index ce0259c690b821..8c03032bb8ac36 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -2,7 +2,15 @@ == Upgrading {kib} Depending on the {kib} version you're upgrading from, the upgrade process to 7.0 -varies. +varies. + +NOTE: {kib} upgrades automatically when starting a new version, as described in +<>. +Although you do not need to manually back up {kib} before upgrading, we recommend +that you have a backup on hand. You can use +<> to back up {kib} +data by targeting `.kibana*` indices. If you are using the Reporting plugin, +you can also target `.reporting*` indices. [float] [[upgrade-before-you-begin]] @@ -12,7 +20,7 @@ Before you upgrade {kib}: * Consult the <>. * Before you upgrade production servers, test the upgrades in a dev environment. -* Backup your data with {es} {ref}/modules-snapshots.html[snapshots]. +* Back up your data with {es} {ref}/modules-snapshots.html[snapshots]. To roll back to an earlier version, you **must** have a backup of your data. * If you are using custom plugins, check that a compatible version is available. diff --git a/package.json b/package.json index 8fa9bf1847eb81..3abd69a6166829 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "**/@types/react": "16.8.3", "**/@types/hapi": "^17.0.18", "**/@types/angular": "^1.6.56", - "**/typescript": "3.5.3", + "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", "**/image-diff/gm/debug": "^2.6.9" @@ -346,8 +346,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.5.0", - "@typescript-eslint/parser": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", @@ -360,7 +360,7 @@ "chance": "1.0.18", "cheerio": "0.22.0", "chokidar": "3.2.1", - "chromedriver": "^77.0.0", + "chromedriver": "78.0.1", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", @@ -444,7 +444,7 @@ "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", "tree-kill": "^1.2.1", - "typescript": "3.5.3", + "typescript": "3.7.2", "typings-tester": "^0.3.2", "vinyl-fs": "^3.0.3", "xml2js": "^0.4.22", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index b5079a49c8385d..c67629f058d5ae 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.5.0", - "@typescript-eslint/parser": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index b0ac86b465a621..f59fbf4720835e 100644 --- a/packages/kbn-analytics/package.json +++ b/packages/kbn-analytics/package.json @@ -17,6 +17,6 @@ "@babel/cli": "7.5.5", "@kbn/dev-utils": "1.0.0", "@kbn/babel-preset": "1.0.0", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/packages/kbn-config-schema/package.json b/packages/kbn-config-schema/package.json index 4880fb4ebfdeeb..71c0ae4bff1f9d 100644 --- a/packages/kbn-config-schema/package.json +++ b/packages/kbn-config-schema/package.json @@ -10,7 +10,7 @@ "kbn:bootstrap": "yarn build" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" }, "peerDependencies": { "joi": "^13.5.2", diff --git a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap index 2836c80f5b5ad6..81fafdb4a7b339 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#scheme returns error when shorter string 1`] = `"expected URI with scheme [http|https] but but got [ftp://elastic.co]."`; +exports[`#scheme returns error when shorter string 1`] = `"expected URI with scheme [http|https] but got [ftp://elastic.co]."`; -exports[`#scheme returns error when shorter string 2`] = `"expected URI with scheme [http|https] but but got [file:///kibana.log]."`; +exports[`#scheme returns error when shorter string 2`] = `"expected URI with scheme [http|https] but got [file:///kibana.log]."`; exports[`#validate throws when returns string 1`] = `"validator failure"`; diff --git a/packages/kbn-config-schema/src/types/uri_type.ts b/packages/kbn-config-schema/src/types/uri_type.ts index f283554de527e3..df1ce9e869d3b4 100644 --- a/packages/kbn-config-schema/src/types/uri_type.ts +++ b/packages/kbn-config-schema/src/types/uri_type.ts @@ -36,7 +36,7 @@ export class URIType extends Type { case 'string.base': return `expected value of type [string] but got [${typeDetect(value)}].`; case 'string.uriCustomScheme': - return `expected URI with scheme [${scheme}] but but got [${value}].`; + return `expected URI with scheme [${scheme}] but got [${value}].`; case 'string.uri': return `value is [${value}] but it must be a valid URI (see RFC 3986).`; } diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index e8781f6d901d92..09753afeb120f4 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -21,7 +21,7 @@ "tslib": "^1.9.3" }, "devDependencies": { - "typescript": "3.5.3", + "typescript": "3.7.2", "@kbn/expect": "1.0.0", "chance": "1.0.18" } diff --git a/packages/kbn-elastic-idx/package.json b/packages/kbn-elastic-idx/package.json index abfaea75357dd8..9532983942d6b9 100644 --- a/packages/kbn-elastic-idx/package.json +++ b/packages/kbn-elastic-idx/package.json @@ -20,7 +20,7 @@ "@babel/core": "^7.5.5", "@babel/plugin-transform-async-to-generator": "^7.5.0", "jest": "^24.9.0", - "typescript": "3.5.3" + "typescript": "3.7.2" }, "jest": { "testEnvironment": "node" diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 8a88626bffbe85..3e25ceb8714df0 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -21,7 +21,7 @@ "del": "^5.1.0", "getopts": "^2.2.4", "supports-color": "^7.0.0", - "typescript": "3.5.3" + "typescript": "3.7.2" }, "dependencies": { "intl-format-cache": "^2.1.0", diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index ac46dd02757cf9..2f9b177be65325 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -58,7 +58,7 @@ "strip-ansi": "^4.0.0", "strong-log-transformer": "^2.1.0", "tempy": "^0.3.0", - "typescript": "3.5.3", + "typescript": "3.7.2", "unlazy-loader": "^0.1.3", "webpack": "^4.41.0", "webpack-cli": "^3.3.9", diff --git a/src/core/public/chrome/constants.ts b/src/core/public/chrome/constants.ts index 3411f6f629a13d..c8e53b38c618eb 100644 --- a/src/core/public/chrome/constants.ts +++ b/src/core/public/chrome/constants.ts @@ -18,6 +18,6 @@ */ export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/'; -export const KIBANA_FEEDBACK_LINK = 'https://www.elastic.co/kibana/feedback'; -export const KIBANA_ASK_ELASTIC_LINK = 'https://www.elastic.co/kibana/ask-elastic'; +export const KIBANA_FEEDBACK_LINK = 'https://www.elastic.co/products/kibana/feedback'; +export const KIBANA_ASK_ELASTIC_LINK = 'https://www.elastic.co/products/kibana/ask-elastic'; export const GITHUB_CREATE_ISSUE_LINK = 'https://github.com/elastic/kibana/issues/new/choose'; diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index ff7fee0198f681..cde35f3cbe995a 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -143,6 +143,7 @@ export class BasePathProxyServer { return responseToolkit.continue; }, ], + validate: { payload: true }, }, path: `${this.httpConfig.basePath}/{kbnPath*}`, }); @@ -175,6 +176,7 @@ export class BasePathProxyServer { return responseToolkit.continue; }, ], + validate: { payload: true }, }, path: `/__UNSAFE_bypassBasePath/{kbnPath*}`, }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 3354324c12407d..da97ab535516c8 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -128,6 +128,8 @@ export class HttpServer { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); const { authRequired = true, tags } = route.options; + // Hapi does not allow payload validation to be specified for 'head' or 'get' requests + const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true }; this.server.route({ handler: route.handler, method: route.method, @@ -135,6 +137,11 @@ export class HttpServer { options: { auth: authRequired ? undefined : false, tags: tags ? Array.from(tags) : undefined, + // TODO: This 'validate' section can be removed once the legacy platform is completely removed. + // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default + // validation applied in ./http_tools#getServerOptions + // (All NP routes are already required to specify their own validation in order to access the payload) + validate, }, }); } diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 88164a76c66f07..22468a5b252f42 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -23,6 +23,7 @@ import Hoek from 'hoek'; import { ServerOptions as TLSOptions } from 'https'; import { ValidationError } from 'joi'; import { HttpConfig } from './http_config'; +import { validateObject } from './prototype_pollution'; /** * Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server. @@ -45,6 +46,11 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { options: { abortEarly: false, }, + // TODO: This payload validation can be removed once the legacy platform is completely removed. + // This is a default payload validation which applies to all LP routes which do not specify their own + // `validate.payload` handler, in order to reduce the likelyhood of prototype pollution vulnerabilities. + // (All NP routes are already required to specify their own validation in order to access the payload) + payload: value => Promise.resolve(validateObject(value)), }, }, state: { diff --git a/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap b/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap new file mode 100644 index 00000000000000..937e040c771ee1 --- /dev/null +++ b/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`can't submit {"__proto__":null} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"constructor":{"prototype":null}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"__proto__":true}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"__proto__":{}}}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"constructor":{"prototype":null}}}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"constructor":{"prototype":null}}} 1`] = `"'constructor.prototype' is an invalid key"`; diff --git a/src/core/server/http/prototype_pollution/index.ts b/src/core/server/http/prototype_pollution/index.ts new file mode 100644 index 00000000000000..e1a33ffba155e8 --- /dev/null +++ b/src/core/server/http/prototype_pollution/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { validateObject } from './validate_object'; diff --git a/src/core/server/http/prototype_pollution/validate_object.test.ts b/src/core/server/http/prototype_pollution/validate_object.test.ts new file mode 100644 index 00000000000000..9e23d6cec64445 --- /dev/null +++ b/src/core/server/http/prototype_pollution/validate_object.test.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { validateObject } from './validate_object'; + +test(`fails on circular references`, () => { + const foo: Record = {}; + foo.myself = foo; + + expect(() => + validateObject({ + payload: foo, + }) + ).toThrowErrorMatchingInlineSnapshot(`"circular reference detected"`); +}); + +[ + { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + { + foo: { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + }, + { constructor: { foo: { prototype: null } } }, + { prototype: { foo: { constructor: null } } }, +].forEach(value => { + ['headers', 'payload', 'query', 'params'].forEach(property => { + const obj = { + [property]: value, + }; + test(`can submit ${JSON.stringify(obj)}`, () => { + expect(() => validateObject(obj)).not.toThrowError(); + }); + }); +}); + +// if we use the object literal syntax to create the following values, we end up +// actually reassigning the __proto__ which makes it be a non-enumerable not-own property +// which isn't what we want to test here +[ + JSON.parse(`{ "__proto__": null }`), + JSON.parse(`{ "foo": { "__proto__": true } }`), + JSON.parse(`{ "foo": { "bar": { "__proto__": {} } } }`), + JSON.parse(`{ "constructor": { "prototype" : null } }`), + JSON.parse(`{ "foo": { "constructor": { "prototype" : null } } }`), + JSON.parse(`{ "foo": { "bar": { "constructor": { "prototype" : null } } } }`), +].forEach(value => { + test(`can't submit ${JSON.stringify(value)}`, () => { + expect(() => validateObject(value)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/src/core/server/http/prototype_pollution/validate_object.ts b/src/core/server/http/prototype_pollution/validate_object.ts new file mode 100644 index 00000000000000..cab6ce295ce92a --- /dev/null +++ b/src/core/server/http/prototype_pollution/validate_object.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +interface StackItem { + value: any; + previousKey: string | null; +} + +// we have to do Object.prototype.hasOwnProperty because when you create an object using +// Object.create(null), and I assume other methods, you get an object without a prototype, +// so you can't use current.hasOwnProperty +const hasOwnProperty = (obj: any, property: string) => + Object.prototype.hasOwnProperty.call(obj, property); + +const isObject = (obj: any) => typeof obj === 'object' && obj !== null; + +// we're using a stack instead of recursion so we aren't limited by the call stack +export function validateObject(obj: any) { + if (!isObject(obj)) { + return; + } + + const stack: StackItem[] = [ + { + value: obj, + previousKey: null, + }, + ]; + const seen = new WeakSet([obj]); + + while (stack.length > 0) { + const { value, previousKey } = stack.pop()!; + + if (!isObject(value)) { + continue; + } + + if (hasOwnProperty(value, '__proto__')) { + throw new Error(`'__proto__' is an invalid key`); + } + + if (hasOwnProperty(value, 'prototype') && previousKey === 'constructor') { + throw new Error(`'constructor.prototype' is an invalid key`); + } + + // iterating backwards through an array is reportedly more performant + const entries = Object.entries(value); + for (let i = entries.length - 1; i >= 0; --i) { + const [key, childValue] = entries[i]; + if (isObject(childValue)) { + if (seen.has(childValue)) { + throw new Error('circular reference detected'); + } + + seen.add(childValue); + } + + stack.push({ + value: childValue, + previousKey: key, + }); + } + } +} diff --git a/src/legacy/core_plugins/console/server/proxy_route.js b/src/legacy/core_plugins/console/server/proxy_route.js index 8ce828879a677a..856128f3d4c031 100644 --- a/src/legacy/core_plugins/console/server/proxy_route.js +++ b/src/legacy/core_plugins/console/server/proxy_route.js @@ -71,6 +71,7 @@ export const createProxyRoute = ({ parse: false, }, validate: { + payload: true, query: Joi.object() .keys({ method: Joi.string() diff --git a/src/legacy/core_plugins/console/server/request.test.ts b/src/legacy/core_plugins/console/server/request.test.ts index 463649a0902954..d5504c0f3a3c26 100644 --- a/src/legacy/core_plugins/console/server/request.test.ts +++ b/src/legacy/core_plugins/console/server/request.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import http from 'http'; +import http, { ClientRequest } from 'http'; import * as sinon from 'sinon'; import { sendRequest } from './request'; import { URL } from 'url'; @@ -24,7 +24,7 @@ import { fail } from 'assert'; describe(`Console's send request`, () => { let sandbox: sinon.SinonSandbox; - let stub: sinon.SinonStub; + let stub: sinon.SinonStub, ClientRequest>; let fakeRequest: http.ClientRequest; beforeEach(() => { diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index ca0ac3c3718497..cd64b1ecf25492 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -82,8 +82,11 @@ function QueryBarTopRowUI(props: Props) { const queryLanguage = props.query && props.query.language; const persistedLog: PersistedLog | undefined = React.useMemo( - () => (queryLanguage ? getQueryLog(uiSettings!, storage, appName, queryLanguage) : undefined), - [queryLanguage] + () => + queryLanguage && uiSettings && storage && appName + ? getQueryLog(uiSettings!, storage, appName, queryLanguage) + : undefined, + [appName, queryLanguage, uiSettings, storage] ); function onClickSubmitButton(event: React.MouseEvent) { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index ea0f6775e4831d..d713139366eef4 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -72,6 +72,7 @@ export interface SearchBarOwnProps { // Show when user has privileges to save showSaveQuery?: boolean; savedQuery?: SavedQuery; + onQueryChange?: (payload: { dateRange: TimeRange; query?: Query }) => void; onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; // User has saved the current state as a saved query onSaved?: (savedQuery: SavedQuery) => void; @@ -206,6 +207,18 @@ class SearchBarUI extends Component { ); } + /* + * This Function is here to show the toggle in saved query form + * in case you the date range (from/to) + */ + private shouldRenderTimeFilterInSavedQueryForm() { + const { dateRangeFrom, dateRangeTo, showDatePicker } = this.props; + return ( + showDatePicker || + (!showDatePicker && dateRangeFrom !== undefined && dateRangeTo !== undefined) + ); + } + public setFilterBarHeight = () => { requestAnimationFrame(() => { const height = @@ -299,6 +312,9 @@ class SearchBarUI extends Component { dateRangeFrom: queryAndDateRange.dateRange.from, dateRangeTo: queryAndDateRange.dateRange.to, }); + if (this.props.onQueryChange) { + this.props.onQueryChange(queryAndDateRange); + } }; public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { @@ -440,7 +456,7 @@ class SearchBarUI extends Component { onSave={this.onSave} onClose={() => this.setState({ showSaveQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} {this.state.showSaveNewQueryModal ? ( @@ -449,7 +465,7 @@ class SearchBarUI extends Component { onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)} onClose={() => this.setState({ showSaveNewQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index d4d1c159906aa9..4d3b72bae64115 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -45,6 +45,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { isDisabled={isDisabled()} onClick={handleClick} data-test-subj={props.testId} + className={props.className} > {capitalize(props.label || props.id!)} diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx index ef6b2eaea1e521..c5ccc3acba6104 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx @@ -23,23 +23,26 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { TmsLayer } from 'ui/vis/map/service_settings'; +import { Vis } from 'ui/vis'; +import { RegionMapVisParams } from '../../../region_map/public/types'; import { SelectOption, SwitchOption } from '../../../kbn_vislib_vis_types/public/components'; -import { RegionMapOptionsProps } from '../../../region_map/public/components/region_map_options'; import { WmsInternalOptions } from './wms_internal_options'; -import { TileMapOptionsProps } from './tile_map_options'; -import { TileMapVisParams } from '../types'; +import { WMSOptions, TileMapVisParams } from '../types'; + +interface Props { + stateParams: TileMapVisParams | RegionMapVisParams; + setValue: (title: 'wms', options: WMSOptions) => void; + vis: Vis; +} const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id }); -function WmsOptions({ stateParams, setValue, vis }: TileMapOptionsProps | RegionMapOptionsProps) { +function WmsOptions({ stateParams, setValue, vis }: Props) { const { wms } = stateParams; const { tmsLayers } = vis.type.editorConfig.collections; const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]); - const setWmsOption = ( - paramName: T, - value: TileMapVisParams['wms'][T] - ) => + const setWmsOption = (paramName: T, value: WMSOptions[T]) => setValue('wms', { ...wms, [paramName]: value, diff --git a/src/legacy/server/http/integration_tests/xsrf.test.js b/src/legacy/server/http/integration_tests/xsrf.test.js index 562a94e198631b..baeb61bff6113c 100644 --- a/src/legacy/server/http/integration_tests/xsrf.test.js +++ b/src/legacy/server/http/integration_tests/xsrf.test.js @@ -57,7 +57,8 @@ describe('xsrf request filter', () => { // Disable payload parsing to make HapiJS server accept any content-type header. payload: { parse: false - } + }, + validate: { payload: null } }, handler: async function () { return 'ok'; @@ -71,7 +72,8 @@ describe('xsrf request filter', () => { // Disable payload parsing to make HapiJS server accept any content-type header. payload: { parse: false - } + }, + validate: { payload: null } }, handler: async function () { return 'ok'; diff --git a/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx b/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx index 071e15f8b97f83..a216ad5d928b6d 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx @@ -29,6 +29,8 @@ import { EuiSpacer, EuiButtonEmpty, EuiFormRow, + EuiToolTip, + EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -154,15 +156,24 @@ function RangesParamEditor({ [isFromValid, isToValid] = validateRange({ from, to }, index); } - const fromPrepend = i18n.translate( + const gtePrependLabel = i18n.translate( 'common.ui.aggTypes.ranges.greaterThanOrEqualPrepend', { defaultMessage: '\u2265', } ); - const toPrepend = i18n.translate('common.ui.aggTypes.ranges.lessThanPrepend', { + const gteTooltipContent = i18n.translate( + 'common.ui.aggTypes.ranges.greaterThanOrEqualTooltip', + { + defaultMessage: 'Greater than or equal to', + } + ); + const ltPrependLabel = i18n.translate('common.ui.aggTypes.ranges.lessThanPrepend', { defaultMessage: '\u003c', }); + const ltTooltipContent = i18n.translate('common.ui.aggTypes.ranges.lessThanTooltip', { + defaultMessage: 'Less than', + }); return ( @@ -179,7 +190,11 @@ function RangesParamEditor({ fullWidth={true} compressed={true} isInvalid={!isFromValid} - prepend={fromPrepend} + prepend={ + + {gtePrependLabel} + + } /> @@ -197,7 +212,11 @@ function RangesParamEditor({ fullWidth={true} compressed={true} isInvalid={!isToValid} - prepend={toPrepend} + prepend={ + + {ltPrependLabel} + + } /> diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index 9adfdc4eedcb33..ff6dff9d8b7490 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -33,15 +33,17 @@ export interface FilterValueFormatter { } export interface FilterMeta { + alias: string | null; + disabled: boolean; + negate: boolean; + // controlledBy is there to identify who owns the filter + controlledBy?: string; // index and type are optional only because when you create a new filter, there are no defaults index?: string; type?: string; - disabled: boolean; - negate: boolean; - alias: string | null; key?: string; - value?: string | ((formatter?: FilterValueFormatter) => string); params?: any; + value?: string | ((formatter?: FilterValueFormatter) => string); } export interface Filter { diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 87ef810682f60b..faceef4f90a6fe 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -21,8 +21,6 @@ import { ExpressionInterpret } from '../interpreter_provider'; import { TimeRange } from '../../../data/public'; import { Adapters } from '../../../inspector/public'; import { Query } from '../../../data/public'; -import { ExpressionAST } from '../../../expressions/public'; -import { ExpressionArgAST } from '../../../../plugins/expressions/public'; import { esFilters } from '../../../../plugins/data/public'; export { ArgumentType } from './arguments'; diff --git a/test/api_integration/apis/general/index.js b/test/api_integration/apis/general/index.js index 86b7565cba6de0..f8daff1a6e8a89 100644 --- a/test/api_integration/apis/general/index.js +++ b/test/api_integration/apis/general/index.js @@ -21,5 +21,6 @@ export default function ({ loadTestFile }) { describe('general', () => { loadTestFile(require.resolve('./cookies')); loadTestFile(require.resolve('./csp')); + loadTestFile(require.resolve('./prototype_pollution')); }); } diff --git a/test/api_integration/apis/general/prototype_pollution.ts b/test/api_integration/apis/general/prototype_pollution.ts new file mode 100644 index 00000000000000..1b732dc81afa92 --- /dev/null +++ b/test/api_integration/apis/general/prototype_pollution.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from 'test/api_integration/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('prototype pollution smoke test', () => { + it('prevents payloads with the "constructor.prototype" pollution vector from being accepted', async () => { + await supertest + .post('/api/sample_data/some_data_id') + .send([ + { + constructor: { + prototype: 'foo', + }, + }, + ]) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: "'constructor.prototype' is an invalid key", + validation: { source: 'payload', keys: [] }, + }); + }); + + it('prevents payloads with the "__proto__" pollution vector from being accepted', async () => { + await supertest + .post('/api/sample_data/some_data_id') + .send(JSON.parse(`{"foo": { "__proto__": {} } }`)) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: "'__proto__' is an invalid key", + validation: { source: 'payload', keys: [] }, + }); + }); + }); +} diff --git a/test/api_integration/ftr_provider_context.d.ts b/test/api_integration/ftr_provider_context.d.ts new file mode 100644 index 00000000000000..60f4914a1d27e1 --- /dev/null +++ b/test/api_integration/ftr_provider_context.d.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/test/plugin_functional/plugins/core_plugin_a/package.json b/test/plugin_functional/plugins/core_plugin_a/package.json index 7dede34f3c0cf9..060ae49f43e8ae 100644 --- a/test/plugin_functional/plugins/core_plugin_a/package.json +++ b/test/plugin_functional/plugins/core_plugin_a/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/core_plugin_b/package.json b/test/plugin_functional/plugins/core_plugin_b/package.json index 18ff8cf7cc5c92..3eb878b9ed5dc4 100644 --- a/test/plugin_functional/plugins/core_plugin_b/package.json +++ b/test/plugin_functional/plugins/core_plugin_b/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/core_plugin_legacy/package.json b/test/plugin_functional/plugins/core_plugin_legacy/package.json index 2ae83b28f7e859..5f784c7b836a5f 100644 --- a/test/plugin_functional/plugins/core_plugin_legacy/package.json +++ b/test/plugin_functional/plugins/core_plugin_legacy/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 196e64af399850..9df9352f76fc27 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "9.0.2", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 33e60128d08065..054276b6209078 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "9.0.2", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index e8d7cc03edad0a..90df352e18a001 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -68,7 +68,7 @@ def getOssCiGroupWorker(ciGroup) { "CI_GROUP=${ciGroup}", "JOB=kibana-ciGroup${ciGroup}", ]) { - runbld "./test/scripts/jenkins_ci_group.sh" + runbld("./test/scripts/jenkins_ci_group.sh", "Execute kibana-ciGroup${ciGroup}") } }) } @@ -79,7 +79,7 @@ def getXpackCiGroupWorker(ciGroup) { "CI_GROUP=${ciGroup}", "JOB=xpack-kibana-ciGroup${ciGroup}", ]) { - runbld "./test/scripts/jenkins_xpack_ci_group.sh" + runbld("./test/scripts/jenkins_xpack_ci_group.sh", "Execute xpack-kibana-ciGroup${ciGroup}") } }) } @@ -93,7 +93,7 @@ def legacyJobRunner(name) { ]) { jobRunner('linux && immutable', false) { try { - runbld('.ci/run.sh', true) + runbld('.ci/run.sh', "Execute ${name}", true) } finally { catchError { uploadAllGcsArtifacts(name) @@ -118,16 +118,29 @@ def jobRunner(label, useRamDisk, closure) { // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm def originalWorkspace = env.WORKSPACE ws('/tmp/workspace') { - sh """ - mkdir -p /dev/shm/workspace - mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist - rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it - ln -s /dev/shm/workspace '${originalWorkspace}' - """ + sh( + script: """ + mkdir -p /dev/shm/workspace + mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist + rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it + ln -s /dev/shm/workspace '${originalWorkspace}' + """, + label: "Move workspace to RAM - /dev/shm/workspace" + ) } } - def scmVars = checkout scm + def scmVars + + // Try to clone from Github up to 8 times, waiting 15 secs between attempts + retry(8) { + try { + scmVars = checkout scm + } catch (ex) { + sleep 15 + throw ex + } + } withEnv([ "CI=true", @@ -225,27 +238,33 @@ def sendKibanaMail() { } } -def bash(script) { - sh "#!/bin/bash\n${script}" +def bash(script, label) { + sh( + script: "#!/bin/bash\n${script}", + label: label + ) } def doSetup() { - runbld "./test/scripts/jenkins_setup.sh" + runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") } def buildOss() { - runbld "./test/scripts/jenkins_build_kibana.sh" + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") } def buildXpack() { - runbld "./test/scripts/jenkins_xpack_build_kibana.sh" + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") } def runErrorReporter() { - bash """ - source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests - """ + bash( + """ + source src/dev/ci_setup/setup_env.sh + node scripts/report_failed_tests + """, + "Report failed tests, if necessary" + ) } return this diff --git a/vars/runbld.groovy b/vars/runbld.groovy index 501e2421ca65b5..e52bc244c65cb8 100644 --- a/vars/runbld.groovy +++ b/vars/runbld.groovy @@ -1,11 +1,17 @@ -def call(script, enableJunitProcessing = false) { +def call(script, label, enableJunitProcessing = false) { def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" - sh "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}" + sh( + script: "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}", + label: label ?: script + ) } def junit() { - sh "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh" + sh( + script: "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh", + label: "Process JUnit reports with runbld" + ) } return this diff --git a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx index 8e5fba31ac5a44..8becf6892ff92c 100644 --- a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx +++ b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx @@ -38,6 +38,6 @@ const { injectGlobal, keyframes, withTheme, -} = styledComponents as ThemedStyledComponentsModule; +} = (styledComponents as unknown) as ThemedStyledComponentsModule; export { css, euiStyled, EuiThemeProvider, injectGlobal, keyframes, withTheme }; diff --git a/x-pack/legacy/plugins/apm/cypress/package.json b/x-pack/legacy/plugins/apm/cypress/package.json index 98dcd495b8594c..ef8955fcbd1b08 100644 --- a/x-pack/legacy/plugins/apm/cypress/package.json +++ b/x-pack/legacy/plugins/apm/cypress/package.json @@ -11,11 +11,11 @@ "@cypress/snapshot": "^2.1.3", "@cypress/webpack-preprocessor": "^4.1.0", "@types/js-yaml": "^3.12.1", - "cypress": "^3.4.1", + "cypress": "^3.5.0", "js-yaml": "^3.13.1", "p-limit": "^2.2.1", "ts-loader": "^6.1.0", - "typescript": "^3.6.3", + "typescript": "3.7.2", "webpack": "^4.40.2" } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 2667d03ef8dde4..daba164a4a00ce 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; +import { asRelativeDateTimeRange } from '../../../../utils/formatters'; interface IBucket { key: number; @@ -51,6 +52,9 @@ interface Props { title: React.ReactNode; } +const tooltipHeader = (bucket: FormattedBucket) => + asRelativeDateTimeRange(bucket.x0, bucket.x); + export function ErrorDistribution({ distribution, title }: Props) { const buckets = getFormattedBuckets( distribution.buckets, @@ -73,6 +77,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time" buckets={buckets} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index 2f06f1d52de6ba..a6c80581585783 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -143,7 +143,7 @@ const ErrorGroupList: React.FC = props => { align: 'right', render: (value?: number) => value ? ( - + ) : ( NOT_AVAILABLE_LABEL ) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index f2524ef1c16f4c..13e7a5bfd894e2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ManagedTable } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; @@ -80,7 +80,11 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx index 161d3711484785..c660455e1eed83 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx @@ -128,7 +128,7 @@ export function AgentConfigurationList({ ), sortable: true, render: (value: number) => ( - + ) }, { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index ca10b06c11cbf2..9116e02870a806 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -10,7 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform'; import { fontSizes, truncate } from '../../../style/variables'; -import { asMillis } from '../../../utils/formatters'; +import { convertTo } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; @@ -66,7 +66,11 @@ const traceListColumns: Array> = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index fc86f4bb78afbe..c9e5175a109219 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -11,7 +11,7 @@ import React, { FunctionComponent, useCallback } from 'react'; import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; @@ -132,8 +132,7 @@ export const TransactionDistribution: FunctionComponent = ( ); const xMax = d3.max(buckets, d => d.x) || 0; - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); const bucketIndex = buckets.findIndex( bucket => @@ -187,18 +186,18 @@ export const TransactionDistribution: FunctionComponent = ( }); } }} - formatX={timeFormatter} + formatX={(time: number) => timeFormatter(time).formatted} formatYShort={formatYShort} formatYLong={formatYLong} verticalLineHover={(bucket: IChartPoint) => bucket.y > 0 && !bucket.sample } backgroundHover={(bucket: IChartPoint) => bucket.y > 0 && bucket.sample} - tooltipHeader={(bucket: IChartPoint) => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={(bucket: IChartPoint) => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} tooltipFooter={(bucket: IChartPoint) => !bucket.sample && i18n.translate( diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index a5e6eb622e8fb1..c64231a6ded863 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -12,7 +12,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; -import { asTime } from '../../../../../../utils/formatters'; +import { asDuration } from '../../../../../../utils/formatters'; import { ErrorCountBadge } from '../../ErrorCountBadge'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; @@ -133,7 +133,7 @@ const SpanActionToolTip: React.SFC = ({ function Duration({ item }: { item: IWaterfallItem }) { return ( - {asTime(item.duration)} + {asDuration(item.duration)} ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index 062d103bfc4487..3d75011f52f193 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform'; import { fontFamilyCode, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -28,6 +28,12 @@ interface Props { isLoading: boolean; } +const toMilliseconds = (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted; + export function TransactionList({ items, isLoading }: Props) { const columns: Array> = useMemo( () => [ @@ -67,7 +73,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'p95', @@ -79,7 +85,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx index c76e62d987aaca..a5a677296825c4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiText } from '@elastic/eui'; import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent'; -import { asTime } from '../../../utils/formatters'; +import { asDuration } from '../../../utils/formatters'; interface Props { duration: number; @@ -29,7 +29,7 @@ const DurationSummaryItem = ({ return ( <> - {asTime(duration)} + {asDuration(duration)}   diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx index c4b750a360efd3..ce6935d1858aac 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx @@ -8,9 +8,10 @@ import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { px, units } from '../../../../public/style/variables'; +import { Maybe } from '../../../../typings/common'; interface Props { - items: Array; + items: Array>; } // TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx new file mode 100644 index 00000000000000..b4678b287dc166 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import moment from 'moment-timezone'; +import { TimestampTooltip } from '../index'; +import { mockNow } from '../../../../utils/testHelpers'; + +describe('TimestampTooltip', () => { + const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) + + beforeAll(() => { + // mock Date.now + mockNow(1570737000000); + + moment.tz.setDefault('America/Los_Angeles'); + }); + + afterAll(() => moment.tz.setDefault('')); + + it('should render component with relative time in body and absolute time in tooltip', () => { + expect(shallow()) + .toMatchInlineSnapshot(` + + 5 hours ago + + `); + }); + + it('should format with precision in milliseconds by default', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); + }); + + it('should format with precision in seconds', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); + }); + + it('should format with precision in minutes', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx deleted file mode 100644 index a7149c7604695f..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; -import moment from 'moment-timezone'; -import { TimestampTooltip, asAbsoluteTime } from './index'; -import { mockNow } from '../../../utils/testHelpers'; - -describe('asAbsoluteTime', () => { - afterAll(() => moment.tz.setDefault('')); - - it('should add a leading plus for timezones with positive UTC offset', () => { - moment.tz.setDefault('Europe/Copenhagen'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 14:00 (UTC+2)' - ); - }); - - it('should add a leading minus for timezones with negative UTC offset', () => { - moment.tz.setDefault('America/Los_Angeles'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 05:00 (UTC-7)' - ); - }); - - it('should use default UTC offset formatting when offset contains minutes', () => { - moment.tz.setDefault('Canada/Newfoundland'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 09:30 (UTC-02:30)' - ); - }); - - it('should respect DST', () => { - moment.tz.setDefault('Europe/Copenhagen'); - const timeWithDST = 1559390400000; // Jun 1, 2019 - const timeWithoutDST = 1575201600000; // Dec 1, 2019 - - expect(asAbsoluteTime({ time: timeWithDST })).toBe( - 'Jun 1, 2019, 14:00:00.000 (UTC+2)' - ); - - expect(asAbsoluteTime({ time: timeWithoutDST })).toBe( - 'Dec 1, 2019, 13:00:00.000 (UTC+1)' - ); - }); -}); - -describe('TimestampTooltip', () => { - const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) - - beforeAll(() => { - // mock Date.now - mockNow(1570737000000); - - moment.tz.setDefault('America/Los_Angeles'); - }); - - afterAll(() => moment.tz.setDefault('')); - - it('should render component with relative time in body and absolute time in tooltip', () => { - expect(shallow()) - .toMatchInlineSnapshot(` - - 5 hours ago - - `); - }); - - it('should format with precision in milliseconds by default', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); - }); - - it('should format with precision in seconds', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); - }); - - it('should format with precision in minutes', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); - }); - - it('should format with precision in days', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019 (UTC-7)'); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx index d7ef6517c2fb83..504ff36c078f0c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx @@ -6,48 +6,20 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; import moment from 'moment-timezone'; +import { asAbsoluteDateTime, TimeUnit } from '../../../utils/formatters'; interface Props { /** * timestamp in milliseconds */ time: number; - precision?: 'days' | 'minutes' | 'seconds' | 'milliseconds'; + timeUnit?: TimeUnit; } -function getPreciseTime(precision: Props['precision']) { - switch (precision) { - case 'days': - return ''; - case 'minutes': - return ', HH:mm'; - case 'seconds': - return ', HH:mm:ss'; - default: - return ', HH:mm:ss.SSS'; - } -} - -function withLeadingPlus(value: number) { - return value > 0 ? `+${value}` : value; -} - -export function asAbsoluteTime({ time, precision = 'milliseconds' }: Props) { - const momentTime = moment(time); - const utcOffsetHours = momentTime.utcOffset() / 60; - const utcOffsetFormatted = Number.isInteger(utcOffsetHours) - ? withLeadingPlus(utcOffsetHours) - : 'Z'; - - return momentTime.format( - `MMM D, YYYY${getPreciseTime(precision)} (UTC${utcOffsetFormatted})` - ); -} - -export function TimestampTooltip({ time, precision = 'milliseconds' }: Props) { +export function TimestampTooltip({ time, timeUnit = 'milliseconds' }: Props) { const momentTime = moment(time); const relativeTimeLabel = momentTime.fromNow(); - const absoluteTimeLabel = asAbsoluteTime({ time, precision }); + const absoluteTimeLabel = asAbsoluteDateTime(time, timeUnit); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 1bcf4e08c91443..c4e7ed86df8b7c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -9,6 +9,7 @@ import numeral from '@elastic/numeral'; import { throttle } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { Maybe } from '../../../../../typings/common'; import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; import { asPercent } from '../../../../utils/formatters'; import { unit } from '../../../../style/variables'; @@ -19,7 +20,7 @@ interface Props { timeseries: TimeSeries[]; } -const tickFormatY = (y: number | null | undefined) => { +const tickFormatY = (y: Maybe) => { return numeral(y || 0).format('0 %'); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index b511bdc4392277..f76a27480137a2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -11,9 +11,8 @@ import d3 from 'd3'; import { HistogramInner } from '../index'; import response from './response.json'; import { - getTimeFormatter, asDecimal, - timeUnit + getDurationFormatter } from '../../../../../utils/formatters'; import { toJson } from '../../../../../utils/testHelpers'; import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index'; @@ -25,8 +24,7 @@ describe('Histogram', () => { beforeEach(() => { const buckets = getFormattedBuckets(response.buckets, response.bucketSize); const xMax = d3.max(buckets, d => d.x); - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); wrapper = mount( { bucketSize={response.bucketSize} transactionId="myTransactionId" onClick={onClick} - formatX={timeFormatter} + formatX={time => timeFormatter(time).formatted} formatYShort={t => `${asDecimal(t)} occ.`} formatYLong={t => `${asDecimal(t)} occurrences`} - tooltipHeader={bucket => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={bucket => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} width={800} /> ); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 51aa4a40fb9234..30dcc99af31b9a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -9,20 +9,21 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform // @ts-ignore import CustomPlot from '../CustomPlot'; import { - asDynamicBytes, + asDecimal, asPercent, + asInteger, + asDynamicBytes, getFixedByteFormatter, - asDecimal, - asTime, - asInteger + asDuration } from '../../../../utils/formatters'; import { Coordinate } from '../../../../../typings/timeseries'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { Maybe } from '../../../../../typings/common'; interface Props { - start: number | string | undefined; - end: number | string | undefined; + start: Maybe; + end: Maybe; chart: GenericMetricsChart; } @@ -64,17 +65,17 @@ function getYTickFormatter(chart: GenericMetricsChart) { return getFixedByteFormatter(max); } case 'percent': { - return (y: number | null | undefined) => asPercent(y || 0, 1); + return (y: Maybe) => asPercent(y || 0, 1); } case 'time': { - return (y: number | null | undefined) => asTime(y); + return (y: Maybe) => asDuration(y); } case 'integer': { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asInteger(y) : y; } default: { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asDecimal(y) : y; } } @@ -89,7 +90,7 @@ function getTooltipFormatter({ yUnit }: GenericMetricsChart) { return (c: Coordinate) => asPercent(c.y || 0, 1); } case 'time': { - return (c: Coordinate) => asTime(c.y); + return (c: Coordinate) => asDuration(c.y); } case 'integer': { return (c: Coordinate) => diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js index 1f8c6db8d20a8a..8ee23d61fe0eb6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js @@ -10,7 +10,7 @@ import { EuiToolTip } from '@elastic/eui'; import Legend from '../Legend'; import { units, px } from '../../../../style/variables'; import styled from 'styled-components'; -import { asTime } from '../../../../utils/formatters'; +import { asDuration } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; const NameContainer = styled.div` @@ -39,7 +39,7 @@ export default function AgentMarker({ agentMark, x }) { content={
{agentMark.name} - {asTime(agentMark.us)} + {asDuration(agentMark.us)}
} > diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js index 1648f427edd7d0..346aec9fb080a2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js @@ -12,7 +12,7 @@ import { XYPlot, XAxis } from 'react-vis'; import LastTickValue from './LastTickValue'; import AgentMarker from './AgentMarker'; import { px } from '../../../../style/variables'; -import { getTimeFormatter } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; // Remove any tick that is too close to topTraceDuration @@ -33,8 +33,9 @@ const getXAxisTickValues = (tickValues, topTraceDuration) => { function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues; - const tickFormat = getTimeFormatter(xMax); + const tickFormatter = getDurationFormatter(xMax); const xAxisTickValues = getXAxisTickValues(tickValues, topTraceDuration); + const topTraceDurationFormatted = tickFormatter(topTraceDuration).formatted; return ( @@ -66,7 +67,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { orientation="top" tickSize={0} tickValues={xAxisTickValues} - tickFormat={tickFormat} + tickFormat={time => tickFormatter(time).formatted} tickPadding={20} style={{ text: { fill: theme.euiColorDarkShade } @@ -76,7 +77,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { {topTraceDuration > 0 && ( )} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js index f5992ac7fc63b5..239e46c25904da 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js @@ -19,7 +19,7 @@ import { } from '../../../../style/variables'; import Legend from '../Legend'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { asAbsoluteTime } from '../../TimestampTooltip'; +import { asAbsoluteDateTime } from '../../../../utils/formatters'; const TooltipElm = styled.div` margin: 0 ${px(unit)}; @@ -87,9 +87,7 @@ export default function Tooltip({ return ( -
- {header || asAbsoluteTime({ time: x, precision: 'seconds' })} -
+
{header || asAbsoluteDateTime(x, 'seconds')}
{showLegends ? ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx index adcce161c7ac1c..d2b6970841bdc2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { asTime, asInteger } from '../../../../../utils/formatters'; +import { asDuration, asInteger } from '../../../../../utils/formatters'; import { fontSizes } from '../../../../../style/variables'; export const ChoroplethToolTip: React.SFC<{ @@ -26,7 +26,7 @@ export const ChoroplethToolTip: React.SFC<{ )}
- {asTime(value)} + {asDuration(value)}
( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 94f30a8a2325a6..b5894a9d91e4a0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -27,13 +27,13 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { asInteger, tpmUnit, - TimeFormatter + TimeFormatter, + getDurationFormatter } from '../../../../utils/formatters'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; import { LicenseContext } from '../../../../context/LicenseContext'; import { TransactionLineChart } from './TransactionLineChart'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { getTimeFormatter } from '../../../../utils/formatters'; import { DurationByCountryMap } from './DurationByCountryMap'; import { TRANSACTION_PAGE_LOAD, @@ -74,12 +74,14 @@ export class TransactionCharts extends Component { }; public getResponseTimeTickFormatter = (formatter: TimeFormatter) => { - return (t: number) => formatter(t); + return (t: number) => formatter(t).formatted; }; public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => { return (p: Coordinate) => { - return isValidCoordinateValue(p.y) ? formatter(p.y) : NOT_AVAILABLE_LABEL; + return isValidCoordinateValue(p.y) + ? formatter(p.y).formatted + : NOT_AVAILABLE_LABEL; }; }; @@ -154,7 +156,7 @@ export class TransactionCharts extends Component { const { responseTimeSeries, tpmSeries } = charts; const { transactionType } = urlParams; const maxY = this.getMaxY(responseTimeSeries); - const formatter = getTimeFormatter(maxY); + const formatter = getDurationFormatter(maxY); return ( <> diff --git a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts index b15231e89365a3..75a558ac81a544 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts @@ -16,7 +16,7 @@ import { RectCoordinate, TimeSeries } from '../../typings/timeseries'; -import { asDecimal, asMillis, tpmUnit } from '../utils/formatters'; +import { asDecimal, tpmUnit, convertTo } from '../utils/formatters'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; @@ -70,6 +70,10 @@ export function getResponseTimeSeries({ }: TimeSeriesAPIResponse) { const { overallAvgDuration } = apmTimeseries; const { avg, p95, p99 } = apmTimeseries.responseTimes; + const formattedDuration = convertTo({ + unit: 'milliseconds', + microseconds: overallAvgDuration + }).formatted; const series: TimeSeries[] = [ { @@ -77,7 +81,7 @@ export function getResponseTimeSeries({ defaultMessage: 'Avg.' }), data: avg, - legendValue: asMillis(overallAvgDuration), + legendValue: formattedDuration, type: 'linemark', color: theme.euiColorVis1 }, diff --git a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts index 01a58ac03d0c3f..295ea1f9f900fd 100644 --- a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts +++ b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts @@ -5,6 +5,7 @@ */ import { compact, isObject } from 'lodash'; +import { Maybe } from '../../typings/common'; export interface KeyValuePair { key: string; @@ -12,7 +13,7 @@ export interface KeyValuePair { } export const flattenObject = ( - item: Record | null | undefined, + item: Maybe>, parentKey?: string ): KeyValuePair[] => { if (item) { diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters.ts deleted file mode 100644 index 34b552230fa77a..00000000000000 --- a/x-pack/legacy/plugins/apm/public/utils/formatters.ts +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../common/i18n'; - -const HOURS_CUT_OFF = 3600000000; // 1 hour (in microseconds) -const MINUTES_CUT_OFF = 60000000; // 1 minute (in microseconds) -const SECONDS_CUT_OFF = 10 * 1000000; // 10 seconds (in microseconds) -const MILLISECONDS_CUT_OFF = 10 * 1000; // 10 milliseconds (in microseconds) -const SPACE = ' '; - -/* - * value: time in microseconds - * withUnit: add unit suffix - * defaultValue: value to use if the specified is null/undefined - */ -type FormatterValue = number | undefined | null; -interface FormatterOptions { - withUnit?: boolean; - defaultValue?: string; -} - -export function asHours( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const hoursLabel = - SPACE + - i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { - defaultMessage: 'h' - }); - const formatted = asDecimal(value / 3600000000); - return `${formatted}${withUnit ? hoursLabel : ''}`; -} - -export function asMinutes( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const minutesLabel = - SPACE + - i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { - defaultMessage: 'min' - }); - const formatted = asDecimal(value / 60000000); - return `${formatted}${withUnit ? minutesLabel : ''}`; -} - -export function asSeconds( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const secondsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { - defaultMessage: 's' - }); - const formatted = asDecimal(value / 1000000); - return `${formatted}${withUnit ? secondsLabel : ''}`; -} - -export function asMillis( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const millisLabel = - SPACE + - i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { - defaultMessage: 'ms' - }); - const formatted = asInteger(value / 1000); - return `${formatted}${withUnit ? millisLabel : ''}`; -} - -export function asMicros( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const microsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { - defaultMessage: 'μs' - }); - const formatted = asInteger(value); - return `${formatted}${withUnit ? microsLabel : ''}`; -} - -export type TimeFormatter = ( - value: FormatterValue, - options?: FormatterOptions -) => string; - -type TimeFormatterBuilder = (max: number) => TimeFormatter; - -export const getTimeFormatter: TimeFormatterBuilder = memoize((max: number) => { - const unit = timeUnit(max); - switch (unit) { - case 'h': - return asHours; - case 'm': - return asMinutes; - case 's': - return asSeconds; - case 'ms': - return asMillis; - case 'us': - return asMicros; - } -}); - -export function timeUnit(max: number) { - if (max > HOURS_CUT_OFF) { - return 'h'; - } else if (max > MINUTES_CUT_OFF) { - return 'm'; - } else if (max > SECONDS_CUT_OFF) { - return 's'; - } else if (max > MILLISECONDS_CUT_OFF) { - return 'ms'; - } else { - return 'us'; - } -} - -export function asTime( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const formatter = getTimeFormatter(value); - return formatter(value, { withUnit, defaultValue }); -} - -export function asDecimal(value: number) { - return numeral(value).format('0,0.0'); -} - -export function asInteger(value: number) { - return numeral(value).format('0,0'); -} - -export function tpmUnit(type?: string) { - return type === 'request' - ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { - defaultMessage: 'rpm' - }) - : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { - defaultMessage: 'tpm' - }); -} - -export function asPercent( - numerator: number, - denominator: number | undefined, - fallbackResult = '' -) { - if (!denominator || isNaN(numerator)) { - return fallbackResult; - } - - const decimal = numerator / denominator; - return numeral(decimal).format('0.0%'); -} - -function asKilobytes(value: number) { - return `${asDecimal(value / 1000)} KB`; -} - -function asMegabytes(value: number) { - return `${asDecimal(value / 1e6)} MB`; -} - -function asGigabytes(value: number) { - return `${asDecimal(value / 1e9)} GB`; -} - -function asTerabytes(value: number) { - return `${asDecimal(value / 1e12)} TB`; -} - -function asBytes(value: number) { - return `${asDecimal(value)} B`; -} - -const bailIfNumberInvalid = (cb: (val: number) => string) => { - return (val: number | null | undefined) => { - if (val === null || val === undefined || isNaN(val)) { - return ''; - } - return cb(val); - }; -}; - -export const asDynamicBytes = bailIfNumberInvalid((value: number) => { - return unmemoizedFixedByteFormatter(value)(value); -}); - -const unmemoizedFixedByteFormatter = (max: number) => { - if (max > 1e12) { - return asTerabytes; - } - - if (max > 1e9) { - return asGigabytes; - } - - if (max > 1e6) { - return asMegabytes; - } - - if (max > 1000) { - return asKilobytes; - } - - return asBytes; -}; - -export const getFixedByteFormatter = memoize((max: number) => { - const formatter = unmemoizedFixedByteFormatter(max); - - return bailIfNumberInvalid(formatter); -}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts new file mode 100644 index 00000000000000..bec9cede00a2be --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment-timezone'; +import { asRelativeDateTimeRange, asAbsoluteDateTime } from '../datetime'; + +describe('date time formatters', () => { + describe('asRelativeDateTimeRange', () => { + beforeAll(() => { + moment.tz.setDefault('Europe/Amsterdam'); + }); + afterAll(() => moment.tz.setDefault('')); + const formatDateToTimezone = (dateTimeString: string) => + moment(dateTimeString).valueOf(); + + describe('YYYY - YYYY', () => { + it('range: 10 years', () => { + const start = formatDateToTimezone('2000-01-01 10:01:01'); + const end = formatDateToTimezone('2010-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2000 - 2010'); + }); + it('range: 5 years', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2015-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2010 - 2015'); + }); + }); + describe('MMM YYYY - MMM YYYY', () => { + it('range: 4 years ', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2014-04-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2010 - Apr 2014'); + }); + it('range: 6 months ', () => { + const start = formatDateToTimezone('2019-01-01 10:01:01'); + const end = formatDateToTimezone('2019-07-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2019 - Jul 2019'); + }); + }); + describe('MMM D, YYYY - MMM D, YYYY', () => { + it('range: 2 days', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-05 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 5, 2019'); + }); + it('range: 1 day', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-03 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 3, 2019'); + }); + }); + describe('MMM D, YYYY, HH:mm - HH:mm (UTC)', () => { + it('range: 9 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 19:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 19:01 (UTC+1)'); + }); + it('range: 5 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 15:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 15:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC)', () => { + it('range: 14 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:15:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:15:01 (UTC+1)'); + }); + it('range: 5 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:06:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:06:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC)', () => { + it('range: 9 seconds', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:10.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:10.002 (UTC+1)' + ); + }); + it('range: 1 second', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:02.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:02.002 (UTC+1)' + ); + }); + }); + }); + + describe('asAbsoluteDateTime', () => { + afterAll(() => moment.tz.setDefault('')); + + it('should add a leading plus for timezones with positive UTC offset', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 14:00 (UTC+2)' + ); + }); + + it('should add a leading minus for timezones with negative UTC offset', () => { + moment.tz.setDefault('America/Los_Angeles'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 05:00 (UTC-7)' + ); + }); + + it('should use default UTC offset formatting when offset contains minutes', () => { + moment.tz.setDefault('Canada/Newfoundland'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 09:30 (UTC-02:30)' + ); + }); + + it('should respect DST', () => { + moment.tz.setDefault('Europe/Copenhagen'); + const timeWithDST = 1559390400000; // Jun 1, 2019 + const timeWithoutDST = 1575201600000; // Dec 1, 2019 + + expect(asAbsoluteDateTime(timeWithDST)).toBe( + 'Jun 1, 2019, 14:00:00.000 (UTC+2)' + ); + + expect(asAbsoluteDateTime(timeWithoutDST)).toBe( + 'Dec 1, 2019, 13:00:00.000 (UTC+1)' + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts new file mode 100644 index 00000000000000..014ecad01d4d70 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { asDuration, convertTo, toMicroseconds } from '../duration'; + +describe('duration formatters', () => { + describe('asDuration', () => { + it('formats correctly with defaults', () => { + expect(asDuration(null)).toEqual('N/A'); + expect(asDuration(undefined)).toEqual('N/A'); + expect(asDuration(0)).toEqual('0 μs'); + expect(asDuration(1)).toEqual('1 μs'); + expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs'); + expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual( + '1,000 ms' + ); + expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual( + '10,000 ms' + ); + expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20.0 s'); + expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10.0 min'); + expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60.0 min'); + expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('1.5 h'); + }); + + it('falls back to default value', () => { + expect(asDuration(undefined, { defaultValue: 'nope' })).toEqual('nope'); + }); + }); + + describe('convertTo', () => { + it('hours', () => { + const unit = 'hours'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'h', + value: '1.0', + formatted: '1.0 h' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'h', + value: '2.0', + formatted: '2.0 h' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '1.2' }) + ).toEqual({ value: '1.2', formatted: '1.2' }); + }); + + it('minutes', () => { + const unit = 'minutes'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'min', + value: '60.0', + formatted: '60.0 min' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'min', + value: '120.0', + formatted: '120.0 min' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('seconds', () => { + const unit = 'seconds'; + const twentySecondsAsMicro = toMicroseconds(20, 'seconds'); + const thirtyFiveSecondsAsMicro = toMicroseconds(35, 'seconds'); + expect(convertTo({ unit, microseconds: twentySecondsAsMicro })).toEqual({ + unit: 's', + value: '20.0', + formatted: '20.0 s' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveSecondsAsMicro }) + ).toEqual({ unit: 's', value: '35.0', formatted: '35.0 s' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('milliseconds', () => { + const unit = 'milliseconds'; + const twentyMilliAsMicro = toMicroseconds(20, 'milliseconds'); + const thirtyFiveMilliAsMicro = toMicroseconds(35, 'milliseconds'); + expect(convertTo({ unit, microseconds: twentyMilliAsMicro })).toEqual({ + unit: 'ms', + value: '20', + formatted: '20 ms' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveMilliAsMicro }) + ).toEqual({ unit: 'ms', value: '35', formatted: '35 ms' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('microseconds', () => { + const unit = 'microseconds'; + expect(convertTo({ unit, microseconds: 20 })).toEqual({ + unit: 'μs', + value: '20', + formatted: '20 μs' + }); + expect(convertTo({ unit, microseconds: 35 })).toEqual({ + unit: 'μs', + value: '35', + formatted: '35 μs' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + }); + describe('toMicroseconds', () => { + it('transformes to microseconds', () => { + expect(toMicroseconds(1, 'hours')).toEqual(3600000000); + expect(toMicroseconds(10, 'minutes')).toEqual(600000000); + expect(toMicroseconds(10, 'seconds')).toEqual(10000000); + expect(toMicroseconds(10, 'milliseconds')).toEqual(10000); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts new file mode 100644 index 00000000000000..f6ed88a850a5b2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { asPercent } from '../formatters'; + +describe('formatters', () => { + describe('asPercent', () => { + it('should divide and format item as percent', () => { + expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); + }); + + it('should format when numerator is 0', () => { + expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); + }); + + it('should return fallback when denominator is undefined', () => { + expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when denominator is 0 ', () => { + expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when numerator or denominator is NaN', () => { + expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); + expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts similarity index 63% rename from x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts rename to x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts index 093624240565f0..07d3d0c1eb08f6 100644 --- a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts @@ -3,61 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { getFixedByteFormatter, asDynamicBytes } from '../size'; -import { - asPercent, - asTime, - getFixedByteFormatter, - asDynamicBytes -} from '../formatters'; - -describe('formatters', () => { - describe('asTime', () => { - it('formats correctly with defaults', () => { - expect(asTime(null)).toEqual('N/A'); - expect(asTime(undefined)).toEqual('N/A'); - expect(asTime(0)).toEqual('0 μs'); - expect(asTime(1)).toEqual('1 μs'); - expect(asTime(1000)).toEqual('1,000 μs'); - expect(asTime(1000 * 1000)).toEqual('1,000 ms'); - expect(asTime(1000 * 1000 * 10)).toEqual('10,000 ms'); - expect(asTime(1000 * 1000 * 20)).toEqual('20.0 s'); - expect(asTime(60000000 * 10)).toEqual('10.0 min'); - expect(asTime(3600000000 * 1.5)).toEqual('1.5 h'); - }); - - it('formats without unit', () => { - expect(asTime(1000, { withUnit: false })).toEqual('1,000'); - }); - - it('falls back to default value', () => { - expect(asTime(undefined, { defaultValue: 'nope' })).toEqual('nope'); - }); - }); - - describe('asPercent', () => { - it('should divide and format item as percent', () => { - expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); - }); - - it('should format when numerator is 0', () => { - expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); - }); - - it('should return fallback when denominator is undefined', () => { - expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when denominator is 0 ', () => { - expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when numerator or denominator is NaN', () => { - expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); - expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); - }); - }); - +describe('size formatters', () => { describe('byte formatting', () => { const bytes = 10; const kb = 1000 + 1; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts new file mode 100644 index 00000000000000..98483a0351f069 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment-timezone'; + +/** + * Returns the timezone set on momentTime. + * (UTC+offset) when offset if bigger than 0. + * (UTC-offset) when offset if lower than 0. + * @param momentTime Moment + */ +function formatTimezone(momentTime: moment.Moment) { + const DEFAULT_TIMEZONE_FORMAT = 'Z'; + + const utcOffsetHours = momentTime.utcOffset() / 60; + + const customTimezoneFormat = + utcOffsetHours > 0 ? `+${utcOffsetHours}` : utcOffsetHours; + + const utcOffsetFormatted = Number.isInteger(utcOffsetHours) + ? customTimezoneFormat + : DEFAULT_TIMEZONE_FORMAT; + + return momentTime.format(`(UTC${utcOffsetFormatted})`); +} + +export type TimeUnit = 'hours' | 'minutes' | 'seconds' | 'milliseconds'; +function getTimeFormat(timeUnit: TimeUnit) { + switch (timeUnit) { + case 'hours': + return 'HH'; + case 'minutes': + return 'HH:mm'; + case 'seconds': + return 'HH:mm:ss'; + case 'milliseconds': + return 'HH:mm:ss.SSS'; + default: + return ''; + } +} + +type DateUnit = 'days' | 'months' | 'years'; +function getDateFormat(dateUnit: DateUnit) { + switch (dateUnit) { + case 'years': + return 'YYYY'; + case 'months': + return 'MMM YYYY'; + case 'days': + return 'MMM D, YYYY'; + default: + return ''; + } +} + +function getFormatsAccordingToDateDifference( + momentStart: moment.Moment, + momentEnd: moment.Moment +) { + const getDateDifference = (unitOfTime: DateUnit | TimeUnit) => + momentEnd.diff(momentStart, unitOfTime); + + if (getDateDifference('years') >= 5) { + return { dateFormat: getDateFormat('years') }; + } + + if (getDateDifference('months') >= 5) { + return { dateFormat: getDateFormat('months') }; + } + + const dateFormatWithDays = getDateFormat('days'); + if (getDateDifference('days') > 1) { + return { dateFormat: dateFormatWithDays }; + } + + if (getDateDifference('hours') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('minutes') + }; + } + + if (getDateDifference('minutes') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('seconds') + }; + } + + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('milliseconds') + }; +} + +export function asAbsoluteDateTime( + time: number, + timeUnit: TimeUnit = 'milliseconds' +) { + const momentTime = moment(time); + const formattedTz = formatTimezone(momentTime); + + return momentTime.format( + `${getDateFormat('days')}, ${getTimeFormat(timeUnit)} ${formattedTz}` + ); +} + +/** + * + * Returns the dates formatted according to the difference between the two dates: + * + * | Difference | Format | + * | -------------- |:----------------------------------------------:| + * | >= 5 years | YYYY - YYYY | + * | >= 5 months | MMM YYYY - MMM YYYY | + * | > 1 day | MMM D, YYYY - MMM D, YYYY | + * | >= 5 hours | MMM D, YYYY, HH:mm - HH:mm (UTC) | + * | >= 5 minutes | MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC) | + * | default | MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC) | + * + * @param start timestamp + * @param end timestamp + */ +export function asRelativeDateTimeRange(start: number, end: number) { + const momentStartTime = moment(start); + const momentEndTime = moment(end); + + const { dateFormat, timeFormat } = getFormatsAccordingToDateDifference( + momentStartTime, + momentEndTime + ); + + if (timeFormat) { + const startFormatted = momentStartTime.format( + `${dateFormat}, ${timeFormat}` + ); + const endFormatted = momentEndTime.format(timeFormat); + const formattedTz = formatTimezone(momentStartTime); + return `${startFormatted} - ${endFormatted} ${formattedTz}`; + } + + const startFormatted = momentStartTime.format(dateFormat); + const endFormatted = momentEndTime.format(dateFormat); + return `${startFormatted} - ${endFormatted}`; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts new file mode 100644 index 00000000000000..39341e1ff4443b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { memoize } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; +import { asDecimal, asInteger } from './formatters'; +import { TimeUnit } from './datetime'; +import { Maybe } from '../../../typings/common'; + +interface FormatterOptions { + defaultValue?: string; +} + +type DurationTimeUnit = TimeUnit | 'microseconds'; + +interface DurationUnit { + [unit: string]: { + label: string; + convert: (value: number) => string; + }; +} + +interface ConvertedDuration { + value: string; + unit?: string; + formatted: string; +} + +export type TimeFormatter = ( + value: Maybe, + options?: FormatterOptions +) => ConvertedDuration; + +type TimeFormatterBuilder = (max: number) => TimeFormatter; + +const durationUnit: DurationUnit = { + hours: { + label: i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { + defaultMessage: 'h' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asHours()) + }, + minutes: { + label: i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { + defaultMessage: 'min' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asMinutes()) + }, + seconds: { + label: i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { + defaultMessage: 's' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asSeconds()) + }, + milliseconds: { + label: i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { + defaultMessage: 'ms' + }), + convert: (value: number) => + asInteger(moment.duration(value / 1000).asMilliseconds()) + }, + microseconds: { + label: i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { + defaultMessage: 'μs' + }), + convert: (value: number) => asInteger(value) + } +}; + +/** + * Converts a microseconds value into the unit defined. + * + * @param param0 + * { unit: "milliseconds" | "hours" | "minutes" | "seconds" | "microseconds", microseconds, defaultValue } + * + * @returns object { value, unit, formatted } + */ +export function convertTo({ + unit, + microseconds, + defaultValue = NOT_AVAILABLE_LABEL +}: { + unit: DurationTimeUnit; + microseconds: Maybe; + defaultValue?: string; +}): ConvertedDuration { + const duration = durationUnit[unit]; + if (!duration || microseconds == null) { + return { value: defaultValue, formatted: defaultValue }; + } + + const convertedValue = duration.convert(microseconds); + return { + value: convertedValue, + unit: duration.label, + formatted: `${convertedValue} ${duration.label}` + }; +} + +export const toMicroseconds = (value: number, timeUnit: TimeUnit) => + moment.duration(value, timeUnit).asMilliseconds() * 1000; + +function getDurationUnitKey(max: number): DurationTimeUnit { + if (max > toMicroseconds(1, 'hours')) { + return 'hours'; + } + if (max > toMicroseconds(1, 'minutes')) { + return 'minutes'; + } + if (max > toMicroseconds(10, 'seconds')) { + return 'seconds'; + } + if (max > toMicroseconds(10, 'milliseconds')) { + return 'milliseconds'; + } + return 'microseconds'; +} + +export const getDurationFormatter: TimeFormatterBuilder = memoize( + (max: number) => { + const unit = getDurationUnitKey(max); + return (value, { defaultValue }: FormatterOptions = {}) => { + return convertTo({ unit, microseconds: value, defaultValue }); + }; + } +); + +/** + * Converts value and returns it formatted - 00 unit + * + * @param value + * @param param1 { defaultValue } + * @returns formated value - 00 unit + */ +export function asDuration( + value: Maybe, + { defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + + const formatter = getDurationFormatter(value); + return formatter(value, { defaultValue }).formatted; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts new file mode 100644 index 00000000000000..630b6a0a18dbf7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; + +export function asDecimal(value: number) { + return numeral(value).format('0,0.0'); +} + +export function asInteger(value: number) { + return numeral(value).format('0,0'); +} + +export function tpmUnit(type?: string) { + return type === 'request' + ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { + defaultMessage: 'rpm' + }) + : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { + defaultMessage: 'tpm' + }); +} + +export function asPercent( + numerator: number, + denominator: number | undefined, + fallbackResult = '' +) { + if (!denominator || isNaN(numerator)) { + return fallbackResult; + } + + const decimal = numerator / denominator; + return numeral(decimal).format('0.0%'); +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts new file mode 100644 index 00000000000000..4fedd55ff1e893 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './formatters'; +export * from './datetime'; +export * from './duration'; +export * from './size'; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts new file mode 100644 index 00000000000000..2cdf8af1d46de8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { memoize } from 'lodash'; +import { asDecimal } from './formatters'; +import { Maybe } from '../../../typings/common'; + +function asKilobytes(value: number) { + return `${asDecimal(value / 1000)} KB`; +} + +function asMegabytes(value: number) { + return `${asDecimal(value / 1e6)} MB`; +} + +function asGigabytes(value: number) { + return `${asDecimal(value / 1e9)} GB`; +} + +function asTerabytes(value: number) { + return `${asDecimal(value / 1e12)} TB`; +} + +function asBytes(value: number) { + return `${asDecimal(value)} B`; +} + +const bailIfNumberInvalid = (cb: (val: number) => string) => { + return (val: Maybe) => { + if (val === null || val === undefined || isNaN(val)) { + return ''; + } + return cb(val); + }; +}; + +export const getFixedByteFormatter = memoize((max: number) => { + const formatter = unmemoizedFixedByteFormatter(max); + + return bailIfNumberInvalid(formatter); +}); + +export const asDynamicBytes = bailIfNumberInvalid((value: number) => { + return unmemoizedFixedByteFormatter(value)(value); +}); + +const unmemoizedFixedByteFormatter = (max: number) => { + if (max > 1e12) { + return asTerabytes; + } + + if (max > 1e9) { + return asGigabytes; + } + + if (max > 1e6) { + return asMegabytes; + } + + if (max > 1000) { + return asKilobytes; + } + + return asBytes; +}; diff --git a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts index 411d03fce349d6..c36efc232b7824 100644 --- a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts +++ b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../../typings/common'; -export const isValidCoordinateValue = ( - value: number | null | undefined -): value is number => value !== null && value !== undefined; +export const isValidCoordinateValue = (value: Maybe): value is number => + value !== null && value !== undefined; diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index dcc034287863af..ab0f47eb04d628 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -7,10 +7,15 @@ import { Legacy } from 'kibana'; import { Server } from 'hapi'; import moment from 'moment'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { getESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; -import { PromiseReturnType } from '../../../typings/common'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { + getApmIndices, + ApmIndicesConfig +} from '../settings/apm_indices/get_apm_indices'; +import { ESFilter } from '../../../typings/elasticsearch'; +import { ESClient } from './es_client'; function decodeUiFilters(server: Server, uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -26,9 +31,20 @@ export interface APMRequestQuery { end?: string; uiFilters?: string; } +// Explicitly type Setup to prevent TS initialization errors +// https://github.com/microsoft/TypeScript/issues/34933 -export type Setup = PromiseReturnType; -export async function setupRequest(req: Legacy.Request) { +export interface Setup { + start: number; + end: number; + uiFiltersES: ESFilter[]; + client: ESClient; + internalClient: ESClient; + config: KibanaConfig; + indices: ApmIndicesConfig; +} + +export async function setupRequest(req: Legacy.Request): Promise { const query = (req.query as unknown) as APMRequestQuery; const { server } = req; const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient( diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 594a0d35ed176c..3764f18a6d692f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -48,9 +48,10 @@ type GenericMetricsRequest = Overwrite< } >; -export function transformDataToMetricsChart< - TRequest extends GenericMetricsRequest ->(result: ESSearchResponse, chartBase: ChartBase) { +export function transformDataToMetricsChart( + result: ESSearchResponse, + chartBase: ChartBase +) { const { aggregations, hits } = result; const timeseriesData = idx(aggregations, _ => _.timeseriesData); diff --git a/x-pack/legacy/plugins/apm/typings/common.d.ts b/x-pack/legacy/plugins/apm/typings/common.d.ts index d79b05ed99b496..b9064980bd6577 100644 --- a/x-pack/legacy/plugins/apm/typings/common.d.ts +++ b/x-pack/legacy/plugins/apm/typings/common.d.ts @@ -27,3 +27,5 @@ export type PromiseReturnType = Func extends ( ) => Promise ? Value : Func; + +export type Maybe = T | null | undefined; diff --git a/x-pack/legacy/plugins/apm/typings/timeseries.ts b/x-pack/legacy/plugins/apm/typings/timeseries.ts index 9b9f7dcc2c8207..d64486d8e71e91 100644 --- a/x-pack/legacy/plugins/apm/typings/timeseries.ts +++ b/x-pack/legacy/plugins/apm/typings/timeseries.ts @@ -3,10 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../typings/common'; export interface Coordinate { x: number; - y: number | null | undefined; + y: Maybe; } export interface RectCoordinate { diff --git a/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts b/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts index 5557befa9d7e5d..dbc0894ab80710 100644 --- a/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts +++ b/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts @@ -43,7 +43,9 @@ export const asChildFunctionRenderer = ( } public render() { - return this.props.children(this.getRendererArgs()); + return (this.props.children as ChildFunctionRendererProps['children'])( + this.getRendererArgs() + ); } private getRendererArgs = () => diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot index 5efbbe01065054..ef301c08cdfe83 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot @@ -11,7 +11,7 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` } >
- The axis is disabled +

+ Switch on to view axis settings +

diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx index 0ec722e370b404..832f953974af4f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx @@ -71,7 +71,11 @@ export class ExtendedTemplate extends PureComponent { const isDisabled = typeof this.props.argValue === 'boolean' && this.props.argValue === false; if (isDisabled) { - return The axis is disabled; + return ( + +

{strings.getDisabledText()}

+
+ ); } const positions = { @@ -85,7 +89,7 @@ export class ExtendedTemplate extends PureComponent { return ( - + + { onChange={ev => setInputValue(ev.target.value)} /> - + - Set + {strings.getButtonSet()} setAddMode(!addMode)} flush="left"> - Cancel + {strings.getButtonCancel()} ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js index e8c433fb8752df..a3c327da2e4dc8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js @@ -130,6 +130,7 @@ class ImageUpload extends React.Component { idSelected={urlType} onChange={this.changeUrlType} isFullWidth + className="canvasSidebar__buttonGroup" /> ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js index d8a7188dfab286..dc31497a7da783 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js @@ -26,7 +26,7 @@ const StringArgInput = ({ updateValue, value, confirm, commit, argId }) => ( /> {confirm && ( - + commit(value)}> {confirm} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js index 462537e82b164f..de19d3e29221bb 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js @@ -19,8 +19,14 @@ const ToggleArgInput = ({ onValueChange, argValue, argId, renderError }) => { return null; } return ( - - + + ); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js index ec492f52747c13..193d99e1c95339 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js @@ -5,28 +5,15 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { ComponentStrings, CANVAS, DataSourceStrings } from '../../../i18n'; +import { DataSourceStrings } from '../../../i18n'; const { DemoData: strings } = DataSourceStrings; const DemodataDatasource = () => ( - -

{strings.getHeading()}

-

- {ComponentStrings.DatasourceDatasourceComponent.getChangeButtonLabel()} - ), - }} - /> -

+ +

{strings.getDescription()}

); @@ -34,7 +21,6 @@ export const demodata = () => ({ name: 'demodata', displayName: strings.getDisplayName(), help: strings.getHelp(), - // Replace this with a better icon when we have time. image: 'logoElasticStack', template: templateFromReactComponent(DemodataDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js index 43f2fa63aff702..707f2305e1368f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js @@ -6,10 +6,10 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { EuiFormRow, EuiTextArea } from '@elastic/eui'; +import { EuiFormRow, EuiTextArea, EuiLink, EuiText } from '@elastic/eui'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { DataSourceStrings } from '../../../i18n'; +import { DataSourceStrings, SQL_URL } from '../../../i18n'; const { Essql: strings } = DataSourceStrings; @@ -59,13 +59,24 @@ class EssqlDatasource extends PureComponent { const { isInvalid } = this.props; return ( - + + + {strings.getLabelAppend()} + +
+ } + >
); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js index 107d4d241d2e7c..13aa2a06306a08 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { timelion } from './timelion'; import { demodata } from './demodata'; import { essql } from './essql'; +import { timelion } from './timelion'; -export const datasourceSpecs = [timelion, demodata, essql]; +export const datasourceSpecs = [demodata, essql, timelion]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js index 06efb6a791a2de..b30e43c1c3c574 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js @@ -12,13 +12,13 @@ import { EuiCallOut, EuiSpacer, EuiCode, - EuiText, EuiTextArea, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; import { DataSourceStrings, TIMELION, CANVAS } from '../../../i18n'; +import { TooltipIcon } from '../../../public/components/tooltip_icon'; const { Timelion: strings } = DataSourceStrings; @@ -57,43 +57,12 @@ const TimelionDatasource = ({ args, updateArgs, defaultIndex }) => { return (
- -

{TIMELION}

-

{strings.getAbout()}

-
- - - - - setArg(argName, e.target.value)} - /> - - { - // TODO: Time timelion interval picker should be a drop down - } - - setArg('interval', e.target.value)} - /> - - - - - +
  • {
  • {
+ + + + } + > + setArg(argName, e.target.value)} + rows={15} + /> + + { + // TODO: Time timelion interval picker should be a drop down + } + + setArg('interval', e.target.value)} + /> +
); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js index 213a2e0dd3b81f..33cdb5541e1726 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js @@ -16,6 +16,13 @@ export const metric = () => ({ modelArgs: [['_', { label: strings.getNumberDisplayName() }]], requiresContext: false, args: [ + { + name: 'metricFormat', + displayName: strings.getMetricFormatDisplayName(), + help: strings.getMetricFormatHelp(), + argType: 'numberFormat', + default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, + }, { name: '_', displayName: strings.getLabelDisplayName(), @@ -23,13 +30,6 @@ export const metric = () => ({ argType: 'string', default: '""', }, - { - name: 'labelFont', - displayName: strings.getLabelFontDisplayName(), - help: strings.getLabelFontHelp(), - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, - }, { name: 'metricFont', displayName: strings.getMetricFontDisplayName(), @@ -38,11 +38,11 @@ export const metric = () => ({ default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, }, { - name: 'metricFormat', - displayName: strings.getMetricFormatDisplayName(), - help: strings.getMetricFormatHelp(), - argType: 'numberFormat', - default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, + name: 'labelFont', + displayName: strings.getLabelFontDisplayName(), + help: strings.getLabelFontHelp(), + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, }, ], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js index 4bb68973e80eab..783140b0c8b9ef 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js @@ -23,6 +23,16 @@ export const pie = () => ({ name: 'palette', argType: 'palette', }, + { + name: 'legend', + displayName: strings.getLegendDisplayName(), + help: strings.getLegendHelp(), + argType: 'select', + default: 'ne', + options: { + choices: legendOptions, + }, + }, { name: 'hole', displayName: strings.getHoleDisplayName(), @@ -34,13 +44,6 @@ export const pie = () => ({ max: 100, }, }, - { - name: 'labels', - displayName: strings.getLabelsDisplayName(), - help: strings.getLabelsHelp(), - argType: 'toggle', - default: true, - }, { name: 'labelRadius', displayName: strings.getLabelRadiusDisplayName(), @@ -52,16 +55,6 @@ export const pie = () => ({ max: 100, }, }, - { - name: 'legend', - displayName: strings.getLegendDisplayName(), - help: strings.getLegendHelp(), - argType: 'select', - default: 'ne', - options: { - choices: legendOptions, - }, - }, { name: 'radius', displayName: strings.getRadiusDisplayName(), @@ -69,6 +62,20 @@ export const pie = () => ({ argType: 'percentage', default: 1, }, + { + name: 'tilt', + displayName: strings.getTiltDisplayName(), + help: strings.getTiltHelp(), + argType: 'percentage', + default: 1, + }, + { + name: 'labels', + displayName: strings.getLabelsDisplayName(), + help: strings.getLabelsHelp(), + argType: 'toggle', + default: true, + }, { name: 'seriesStyle', argType: 'seriesStyle', @@ -78,13 +85,6 @@ export const pie = () => ({ name: 'font', argType: 'font', }, - { - name: 'tilt', - displayName: strings.getTiltDisplayName(), - help: strings.getTiltHelp(), - argType: 'percentage', - default: 1, - }, ], resolve({ context }) { if (getState(context) !== 'ready') { diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index 1e6da888abf58e..5b9f6f00940f4b 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -228,11 +228,11 @@ export const ComponentStrings = { DatasourceDatasourceComponent: { getChangeButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.changeButtonLabel', { - defaultMessage: 'Change your data source', + defaultMessage: 'Change element data source', }), getPreviewButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', { - defaultMessage: 'Preview', + defaultMessage: 'Preview data', }), getSaveButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', { @@ -294,7 +294,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.elementConfig.title', { - defaultMessage: 'Elements', + defaultMessage: 'Element status', description: '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad', }), @@ -581,7 +581,7 @@ export const ComponentStrings = { }), getBackgroundColorLabel: () => i18n.translate('xpack.canvas.pageConfig.backgroundColorLabel', { - defaultMessage: 'Background Color', + defaultMessage: 'Background', }), getNoTransitionDropDownOptionLabel: () => i18n.translate('xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel', { @@ -592,7 +592,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.pageConfig.title', { - defaultMessage: 'Page', + defaultMessage: 'Page styles', }), getTransitionLabel: () => i18n.translate('xpack.canvas.pageConfig.transitionLabel', { @@ -1002,7 +1002,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.workpadConfig.title', { - defaultMessage: 'Workpad', + defaultMessage: 'Workpad settings', }), getUSLetterButtonLabel: () => i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', { diff --git a/x-pack/legacy/plugins/canvas/i18n/constants.ts b/x-pack/legacy/plugins/canvas/i18n/constants.ts index 3659c369ba0b6c..8aee6ca1486811 100644 --- a/x-pack/legacy/plugins/canvas/i18n/constants.ts +++ b/x-pack/legacy/plugins/canvas/i18n/constants.ts @@ -15,6 +15,7 @@ export const CSV = 'CSV'; export const DATEMATH = '`datemath`'; export const DATATABLE = '`datatable`'; export const ELASTICSEARCH = 'Elasticsearch'; +export const ELASTICSEARCH_SHORT = 'ES'; export const FONT_FAMILY = '`font-family`'; export const FONT_WEIGHT = '`font-weight`'; export const HEX = 'HEX'; @@ -32,6 +33,8 @@ export const PDF = 'PDF'; export const POST = 'POST'; export const RGB = 'RGB'; export const SQL = 'SQL'; +export const SQL_URL = + 'https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-spec.html'; export const SVG = 'SVG'; export const TIMELION = 'Timelion'; export const TINYMATH = '`TinyMath`'; diff --git a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts index 6bc40a2758ab39..bdd190f26c97aa 100644 --- a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts +++ b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { LUCENE } from './constants'; +import { LUCENE, ELASTICSEARCH } from './constants'; export const ArgTypesStrings = { Color: { @@ -101,13 +101,17 @@ export const ArgTypesStrings = { i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.colorLabel', { defaultMessage: 'Color', }), + getColorValueDefault: () => + i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.colorValueDefault', { + defaultMessage: 'Auto', + }), getStyleLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.styleLabel', { defaultMessage: 'Style', }), getRemoveAriaLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.removeAriaLabel', { - defaultMessage: 'Remove Series Color', + defaultMessage: 'Remove series color', }), getNoSeriesTooltip: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.noSeriesTooltip', { @@ -115,11 +119,11 @@ export const ArgTypesStrings = { }), getSeriesIdentifierLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.seriesIdentifierLabel', { - defaultMessage: 'Series Identifier', + defaultMessage: 'Series id', }), getSelectSeriesOption: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.selectSeriesDropDown', { - defaultMessage: 'Select Series', + defaultMessage: 'Select series', }), getLineLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.lineLabel', { @@ -152,15 +156,18 @@ export const ExpressionDataSourceStrings = { }), getWarningTitle: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningTitle', { - defaultMessage: 'Be careful', + defaultMessage: 'Query with caution', }), getWarning: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningDescription', { defaultMessage: ` - The Elasticsearch Docs datasource is used to pull documents directly from Elasticsearch + This datasource pulls directly from {elasticsearch} without the use of aggregations. It is best used with low volume datasets and in situations where you need to view raw documents or plot exact, non-aggregated values on a chart.`, + values: { + elasticsearch: ELASTICSEARCH, + }, }), getIndexTitle: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.indexTitle', { diff --git a/x-pack/legacy/plugins/canvas/i18n/ui.ts b/x-pack/legacy/plugins/canvas/i18n/ui.ts index b65a666aa88095..323a6c97fd967d 100644 --- a/x-pack/legacy/plugins/canvas/i18n/ui.ts +++ b/x-pack/legacy/plugins/canvas/i18n/ui.ts @@ -12,9 +12,9 @@ import { CANVAS, CSS, ELASTICSEARCH, + ELASTICSEARCH_SHORT, HEX, HTML, - KIBANA, LUCENE, MARKDOWN, MOMENTJS, @@ -35,6 +35,10 @@ export const ArgumentStrings = { i18n.translate('xpack.canvas.uis.arguments.axisConfigLabel', { defaultMessage: 'Visualization axis configuration', }), + getDisabledText: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfigDisabledText', { + defaultMessage: 'Switch on to view axis settings', + }), getPositionBottom: () => i18n.translate('xpack.canvas.uis.arguments.axisConfig.position.options.bottomDropDown', { defaultMessage: 'bottom', @@ -124,6 +128,14 @@ export const ArgumentStrings = { i18n.translate('xpack.canvas.uis.arguments.filterGroup.createNewGroupLinkText', { defaultMessage: 'Create new group', }), + getButtonSet: () => + i18n.translate('xpack.canvas.uis.arguments.filterGroup.setValue', { + defaultMessage: 'Set', + }), + getButtonCancel: () => + i18n.translate('xpack.canvas.uis.arguments.filterGroup.cancelValue', { + defaultMessage: 'Cancel', + }), getDisplayName: () => i18n.translate('xpack.canvas.uis.arguments.filterGroupTitle', { defaultMessage: 'Filter Group', @@ -260,7 +272,7 @@ export const ArgumentStrings = { }), getHelp: () => i18n.translate('xpack.canvas.uis.arguments.shapeLabel', { - defaultMessage: 'Shape picker', + defaultMessage: 'Change the shape of the current element', }), }, String: { @@ -303,12 +315,20 @@ export const DataSourceStrings = { }), getHeading: () => i18n.translate('xpack.canvas.uis.dataSources.demoData.headingTitle', { - defaultMessage: 'You are using demo data', + defaultMessage: 'This element is using demo data', }), getHelp: () => i18n.translate('xpack.canvas.uis.dataSources.demoDataLabel', { defaultMessage: 'Mock data set with usernames, prices, projects, countries, and phases', }), + getDescription: () => + i18n.translate('xpack.canvas.uis.dataSources.demoDataDescription', { + defaultMessage: + 'By default, every {canvas} element is connected to the demo data source. Change the data source, above, to connect your own data.', + values: { + canvas: CANVAS, + }, + }), }, Essql: { getDisplayName: () => @@ -329,9 +349,13 @@ export const DataSourceStrings = { }), getLabel: () => i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitle', { - defaultMessage: '{elasticsearch} {sql} query', + defaultMessage: 'Query', + }), + getLabelAppend: () => + i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitleAppend', { + defaultMessage: 'Learn {elasticsearchShort} {sql} syntax', values: { - elasticsearch: ELASTICSEARCH, + elasticsearchShort: ELASTICSEARCH_SHORT, sql: SQL, }, }), @@ -340,10 +364,9 @@ export const DataSourceStrings = { getAbout: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.aboutDetail', { defaultMessage: - '{canvas} integrates with {kibanaTimelion} application to allow you to use {timelion} queries to pull back timeseries data in a tabular format that can be used with {canvas} elements.', + 'Use {timelion} queries to pull back timeseries data that can be used with {canvas} elements.', values: { timelion: TIMELION, - kibanaTimelion: `${KIBANA}'s ${TIMELION}`, canvas: CANVAS, }, }), @@ -357,9 +380,8 @@ export const DataSourceStrings = { getIntervalHelp: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.intervalLabel', { defaultMessage: - 'Accepts {elasticsearch} date math: {weeksExample}, {daysExample}, {secondsExample}, or {auto}', + 'Use date math like {weeksExample}, {daysExample}, {secondsExample}, or {auto}', values: { - elasticsearch: ELASTICSEARCH, secondsExample: '10s', daysExample: '5d', weeksExample: '1w', @@ -383,7 +405,11 @@ export const DataSourceStrings = { }), getTipsHeading: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.tipsTitle', { - defaultMessage: 'Some tips', + defaultMessage: 'Tips for using {timelion} in {canvas}', + values: { + timelion: TIMELION, + canvas: CANVAS, + }, }), }, }; @@ -530,7 +556,7 @@ export const ViewStrings = { }), getValueDisplayName: () => i18n.translate('xpack.canvas.uis.views.dropdownControl.args.valueColumnTitle', { - defaultMessage: 'Values column', + defaultMessage: 'Value column', }), getValueHelp: () => i18n.translate('xpack.canvas.uis.views.dropdownControl.args.valueColumnLabel', { @@ -610,7 +636,7 @@ export const ViewStrings = { }), getNumberDisplayName: () => i18n.translate('xpack.canvas.uis.views.numberArgTitle', { - defaultMessage: 'Number', + defaultMessage: 'Value', }), getLabelDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelArgTitle', { @@ -618,7 +644,7 @@ export const ViewStrings = { }), getLabelFontDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelFontTitle', { - defaultMessage: 'Label text settings', + defaultMessage: 'Label text', }), getLabelFontHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelFontLabel', { @@ -626,11 +652,11 @@ export const ViewStrings = { }), getLabelHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelArgLabel', { - defaultMessage: 'Describes the metric', + defaultMessage: 'Enter a text label for the metric value', }), getMetricFontDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFontTitle', { - defaultMessage: 'Metric text settings', + defaultMessage: 'Metric text', }), getMetricFontHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFontLabel', { @@ -638,11 +664,11 @@ export const ViewStrings = { }), getMetricFormatDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFormatTitle', { - defaultMessage: 'Metric Format', + defaultMessage: 'Format', }), getMetricFormatHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFormatLabel', { - defaultMessage: 'Fonts, alignment and color', + defaultMessage: 'Select a format for the metric value', }), }, Pie: { @@ -676,7 +702,7 @@ export const ViewStrings = { }), getLegendDisplayName: () => i18n.translate('xpack.canvas.uis.views.pie.args.legendTitle', { - defaultMessage: 'Legend position', + defaultMessage: 'Legend', }), getLegendHelp: () => i18n.translate('xpack.canvas.uis.views.pie.args.legendLabel', { @@ -714,7 +740,7 @@ export const ViewStrings = { }), getLegendDisplayName: () => i18n.translate('xpack.canvas.uis.views.plot.args.legendTitle', { - defaultMessage: 'Legend position', + defaultMessage: 'Legend', }), getLegendHelp: () => i18n.translate('xpack.canvas.uis.views.plot.args.legendLabel', { @@ -744,7 +770,7 @@ export const ViewStrings = { }), getBarColorHelp: () => i18n.translate('xpack.canvas.uis.views.progress.args.barColorLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getBarWeightDisplayName: () => i18n.translate('xpack.canvas.uis.views.progress.args.barWeightTitle', { @@ -930,7 +956,7 @@ export const ViewStrings = { }), getBorderHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.borderLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getBorderWidthDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.borderWidthTitle', { @@ -950,22 +976,19 @@ export const ViewStrings = { }), getFillHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.fillLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getMaintainAspectDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectTitle', { - defaultMessage: 'Maintain aspect ratio', + defaultMessage: 'Fixed ratio', }), getMaintainAspectHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectLabel', { - defaultMessage: `Select '{true}' to maintain aspect ratio`, - values: { - true: BOOLEAN_TRUE, - }, + defaultMessage: `Enable to maintain aspect ratio`, }), getShapeDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.shapeTitle', { - defaultMessage: 'Select a shape', + defaultMessage: 'Select shape', }), }, Table: { @@ -988,7 +1011,7 @@ export const ViewStrings = { }), getPerPageDisplayName: () => i18n.translate('xpack.canvas.uis.views.table.args.perPageTitle', { - defaultMessage: 'Rows per page', + defaultMessage: 'Rows', }), getPerPageHelp: () => i18n.translate('xpack.canvas.uis.views.table.args.perPageLabel', { @@ -1022,7 +1045,7 @@ export const ViewStrings = { }), getFilterGroupDisplayName: () => i18n.translate('xpack.canvas.uis.views.timefilter.args.filterGroupTitle', { - defaultMessage: 'Filter group name', + defaultMessage: 'Filter group', }), getFilterGroupHelp: () => i18n.translate('xpack.canvas.uis.views.timefilter.args.filterGroupLabel', { diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss index f2cdb1444ef23a..15676d2b02490f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss +++ b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss @@ -1,3 +1,7 @@ +.canvasArg__addArg { + margin-right: -$euiSizeS; +} + .canvasArg__addPopover { width: 250px; } diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index e771c978d491b2..9cc6f870b9bded 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -33,6 +33,7 @@ export const ArgAddPopover = ({ options }: Props) => { iconType="plusInCircle" aria-label={strings.getAddAriaLabel()} onClick={handleClick} + className="canvasArg__addArg" /> ); diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss index bef58d6bb6f5fa..3fc220d41d551a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss @@ -8,6 +8,14 @@ .canvasSidebar__panel { .canvasArg--expandable:last-child { + .canvasArg__accordion { + margin-bottom: (-$euiSizeS); + } + + .canvasArg__accordion:after { + content: none; + } + .canvasArg__accordion.euiAccordion-isOpen:after { display: none; } @@ -15,12 +23,12 @@ } .canvasArg { - margin-top: $euiSize; - + margin-top: $euiSizeS; +} - .canvasArg--remove { - visibility: hidden; - } +.canvasArg__remove { + min-width: $euiSize; + padding: $euiSizeXS 0; } .canvasArg__content { @@ -29,47 +37,18 @@ .canvasArg__form { position: relative; - -} - -.canvasArg__form, -.canvasArg__accordion { - &:hover { - .canvasArg__remove { - opacity: 1; - visibility: visible; - } - } } .canvasArg__tooltip { margin-left: -$euiSizeXL; } -.canvasArg__remove { - position: absolute; - right: -$euiSizeL; - top: $euiSizeS - 2px; - border-radius: $euiBorderRadius; - border: $euiBorderThin; - background: $euiColorEmptyShade; - opacity: 0; - visibility: hidden; - transition: opacity $euiAnimSpeedNormal $euiAnimSlightResistance; - transition-delay: $euiAnimSpeedSlow; -} - .canvasArg__accordion { - padding: $euiSizeS $euiSize; - margin: 0 (-$euiSize); + padding: $euiSizeS $euiSizeM; + margin: 0 (-$euiSizeM); background: $euiColorLightestShade; position: relative; - // different spacing means leff shift - .canvasArg__remove { - right: -$euiSizeM; - } - // don't let remove button position here if this is nested in an accordion .canvasArg__form { position: static; @@ -97,3 +76,8 @@ bottom: 0; } } + +// this is a workaround since an EuiFormRow label cannot be passed in toggle.js +.canvasArg__switch { + padding-top: calc(#{$euiSizeS} * .75); +} diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js index 143ce670d25f66..4324eed0892a59 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiFormRow, EuiAccordion, EuiText, EuiToolTip } from '@elastic/eui'; +import { EuiFormRow, EuiAccordion, EuiText, EuiToolTip, EuiIcon } from '@elastic/eui'; // This is what is being generated by render() from the Arg class. It is called in FunctionForm export const ArgLabel = props => { @@ -32,7 +32,17 @@ export const ArgLabel = props => { ) : ( simpleArg && ( - + + + {label} + + + } + id={argId} + > {simpleArg} ) diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx index 5b45772c143730..846f912db6f84b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx @@ -6,7 +6,7 @@ import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { TooltipIcon, IconType } from '../tooltip_icon'; import { ComponentStrings } from '../../../i18n'; @@ -41,13 +41,16 @@ export const ArgSimpleForm: React.FunctionComponent = ({ )} {!required && ( - + + + )} ); diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss index ee6c082db12178..2407dcbbce5939 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss @@ -6,6 +6,31 @@ padding: 0 $euiSizeS; } +.canvasDataSource__section { + padding: $euiSizeM; +} + +.canvasDataSource__triggerButton { + @include euiTitle('xs'); + line-height: $euiSizeXXL; +} + +.canvasDataSource__triggerButtonIcon { + margin-right: $euiSizeS; +} + +.canvasDataSource__list { + padding: $euiSizeM; +} + +.canvasDataSource__card .euiCard__content { + padding-top: 0 !important; // sass-lint:disable-line no-important +} + .canvasDataSource__card + .canvasDataSource__card { margin-top: $euiSizeS; } + +.canvasDataSource__card--isCurrent { + border-color: $euiColorSecondary; +} diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js index 5f235d44791713..8b0061e047f33f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js @@ -7,20 +7,23 @@ import React, { Fragment, PureComponent } from 'react'; import PropTypes from 'prop-types'; import { - EuiPanel, EuiFlexGroup, EuiFlexItem, EuiButton, - EuiButtonEmpty, EuiSpacer, + EuiIcon, + EuiCallOut, + EuiButtonEmpty, + EuiHorizontalRule, } from '@elastic/eui'; import { isEqual } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; +import { ComponentStrings, DataSourceStrings } from '../../../i18n'; import { getDefaultIndex } from '../../lib/es_service'; import { DatasourceSelector } from './datasource_selector'; import { DatasourcePreview } from './datasource_preview'; const { DatasourceDatasourceComponent: strings } = ComponentStrings; +const { DemoData: demoDataStrings } = DataSourceStrings; export class DatasourceComponent extends PureComponent { static propTypes = { @@ -113,7 +116,13 @@ export class DatasourceComponent extends PureComponent { const { defaultIndex } = this.state; if (selecting) { - return ; + return ( + + ); } const datasourcePreview = previewing ? ( @@ -124,47 +133,51 @@ export class DatasourceComponent extends PureComponent { /> ) : null; + const datasourceRender = stateDatasource.render({ + args: stateArgs, + updateArgs, + datasourceDef, + isInvalid, + setInvalid, + defaultIndex, + }); + return ( - +
setSelecting(!selecting)} + className="canvasDataSource__triggerButton" + flush="left" + size="s" > - {strings.getChangeButtonLabel()} + + {stateDatasource.displayName} - {stateDatasource.render({ - args: stateArgs, - updateArgs, - datasourceDef, - isInvalid, - setInvalid, - defaultIndex, - })} - + {stateDatasource.name === 'demodata' ? ( + + {datasourceRender} + + ) : ( + datasourceRender + )} + - setPreviewing(true)} icon="check"> + setPreviewing(true)}> {strings.getPreviewButtonLabel()} - + - + {strings.getSaveButtonLabel()} - +
{datasourcePreview}
diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index e6d2fe550a9356..13cd2c5cd11f7b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -16,6 +16,7 @@ import { EuiPanel, EuiText, EuiEmptyPrompt, + EuiSpacer, } from '@elastic/eui'; import { Datatable } from '../../datatable'; import { Error } from '../../error'; @@ -31,21 +32,22 @@ export const DatasourcePreview = ({ done, datatable }) => ( {strings.getModalTitle()} - +

{datasourceStrings.getSaveButtonLabel()}, }} />

+ {datatable.type === 'error' ? ( ) : ( - + {datatable.rows.length > 0 ? ( ) : ( diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js index 07df2a7007c4f2..92f9b92cb1f06e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js @@ -8,17 +8,21 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiCard, EuiIcon } from '@elastic/eui'; -export const DatasourceSelector = ({ onSelect, datasources }) => ( -
+export const DatasourceSelector = ({ onSelect, datasources, current }) => ( +
{datasources.map(d => ( } - onClick={() => onSelect(d.name)} + titleElement="h5" + icon={} description={d.help} layout="horizontal" className="canvasDataSource__card" + selectable={{ + isSelected: d.name === current ? true : false, + onClick: () => onSelect(d.name), + }} /> ))}
@@ -27,4 +31,5 @@ export const DatasourceSelector = ({ onSelect, datasources }) => ( DatasourceSelector.propTypes = { onSelect: PropTypes.func.isRequired, datasources: PropTypes.array.isRequired, + current: PropTypes.string.isRequired, }; diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js b/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js index caafa068c6b5ba..f531ee1668aef1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js @@ -6,18 +6,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiPanel, EuiText } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; const { DatasourceNoDatasource: strings } = ComponentStrings; export const NoDatasource = () => ( - - -

{strings.getPanelTitle()}

+
+

{strings.getPanelDescription()}

- - +
+
); NoDatasource.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss b/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss index daccfdff5d34b7..bd11bff18e0917 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss +++ b/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss @@ -4,6 +4,7 @@ display: flex; flex-direction: column; justify-content: space-between; + font-size: $euiFontSizeS; .canvasDataTable__tableWrapper { @include euiScrollBar; @@ -12,7 +13,8 @@ overflow: auto; // removes white square in the scrollbar corner - &::-webkit-scrollbar-corner { // sass-lint:disable-line no-vendor-prefixes + // sass-lint:disable no-vendor-prefixes + &::-webkit-scrollbar-corner { background: transparent; } } @@ -21,6 +23,8 @@ width: 100%; display: flex; justify-content: space-around; + padding: $euiSizeS; + border-top: $euiBorderThin; } .canvasDataTable__table { @@ -30,7 +34,8 @@ .canvasDataTable__th, .canvasDataTable__td { text-align: left; - padding: $euiSizeS; + padding: $euiSizeS $euiSizeXS; + border-bottom: $euiBorderThin; } .canvasDataTable__th { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js b/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js index 76007994e56bf3..5d710ef8835482 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion, EuiText, EuiSpacer } from '@elastic/eui'; import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; +import React from 'react'; import { ComponentStrings } from '../../../i18n'; const { ElementConfig: strings } = ComponentStrings; @@ -20,26 +20,51 @@ export const ElementConfig = ({ elementStats }) => { const progress = total > 0 ? Math.round(((ready + error) / total) * 100) : 100; return ( - - -

{strings.getTitle()}

-
- - + + {strings.getTitle()} +
+ } + initialIsOpen={false} + > + + - + - + - + - + - + ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js b/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js index 636d9b0006ac61..11c8ab88a4cba6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js @@ -28,6 +28,7 @@ export const ESFieldSelect = ({ value, fields = [], onChange, onFocus, onBlur }) onBlur={onBlur} singleSelection={{ asPlainText: true }} isClearable={false} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js b/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js index fedb4aba7d3d09..ca2cac5a647931 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js @@ -25,6 +25,7 @@ export const ESFieldsSelect = ({ selected, fields, onChange, onFocus, onBlur }) className="canvasFieldsSelect" onFocus={onFocus} onBlur={onBlur} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js b/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js index edc4506f20bda4..8f1a4932a5e6ce 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js @@ -32,6 +32,7 @@ export const ESIndexSelect = ({ value, loading, indices, onChange, onFocus, onBl singleSelection={{ asPlainText: true }} isClearable={false} onCreateOption={input => onChange(input || defaultIndex)} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js b/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js index 2586b4ec61f04e..583bf1427aab1f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js @@ -6,7 +6,15 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { EuiCard, EuiFormRow, EuiTitle, EuiSpacer, EuiSelect } from '@elastic/eui'; +import { + EuiCard, + EuiFormRow, + EuiTitle, + EuiSpacer, + EuiSelect, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; import { WorkpadColorPicker } from '../workpad_color_picker'; import { ComponentStrings } from '../../../i18n'; @@ -22,14 +30,20 @@ export const PageConfig = ({ }) => { return ( - +

{strings.getTitle()}

- + + + {strings.getBackgroundColorLabel()}{' '} + + + + } > diff --git a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot index 426c07dac497ec..a23452a43ae1e4 100644 --- a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot @@ -13,20 +13,24 @@ exports[`Storyshots components/Shapes/ShapePickerPopover default 1`] = `
- + +
`; @@ -44,27 +48,31 @@ exports[`Storyshots components/Shapes/ShapePickerPopover interactive 1`] = `
- + /> + +
`; @@ -82,27 +90,31 @@ exports[`Storyshots components/Shapes/ShapePickerPopover shape selected 1`] = `
- + /> + +
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx index 472a14071208a0..970f72da698ba0 100644 --- a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx @@ -6,7 +6,7 @@ import React, { MouseEvent } from 'react'; import PropTypes from 'prop-types'; -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiPanel } from '@elastic/eui'; import { Popover } from '../popover'; import { ShapePicker } from '../shape_picker'; import { ShapePreview } from '../shape_preview'; @@ -21,9 +21,11 @@ interface Props { export const ShapePickerPopover = ({ shapes, onChange, value }: Props) => { const button = (handleClick: (ev: MouseEvent) => void) => ( - - - + + + + + ); return ( diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot index dc80af01f121f6..1655320700f87f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot @@ -2,13 +2,17 @@ exports[`Storyshots components/Sidebar/GroupSettings default 1`] = `
-

- Ungroup (U) to edit individual element settings. -

-

- Save this group as a new element to re-use it throughout your workpad. -

+
+

+ Ungroup (U) to edit individual element settings. +

+

+ Save this group as a new element to re-use it throughout your workpad. +

+
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot index b9b13ae36e7308..49e804640081d2 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot @@ -2,13 +2,17 @@ exports[`Storyshots components/Sidebar/MultiElementSettings default 1`] = `
-

- Multiple elements are currently selected. -

-

- Deselect these elements to edit their individual settings, press (G) to group them, or save this selection as a new element to re-use it throughout your workpad. -

+
+

+ Multiple elements are currently selected. +

+

+ Deselect these elements to edit their individual settings, press (G) to group them, or save this selection as a new element to re-use it throughout your workpad. +

+
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index 6d884c05cd13ac..74f4887601d30c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { EuiTabbedContent } from '@elastic/eui'; // @ts-ignore unconverted component import { Datasource } from '../../datasource'; // @ts-ignore unconverted component @@ -30,7 +30,6 @@ export const ElementSettings: FunctionComponent = ({ element }) => { name: strings.getDisplayTabLabel(), content: (
-
@@ -42,7 +41,6 @@ export const ElementSettings: FunctionComponent = ({ element }) => { name: strings.getDataTabLabel(), content: (
-
), diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx index a5920ee1974607..2e241681ccc6a7 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx @@ -20,10 +20,10 @@ export const GlobalConfig: FunctionComponent = () => ( - + - + ); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx index b46465d9ec7755..95d9035774a6ae 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx @@ -11,8 +11,10 @@ import { ComponentStrings } from '../../../i18n'; const { GroupSettings: strings } = ComponentStrings; export const GroupSettings: FunctionComponent = () => ( - -

{strings.getUngroupDescription()}

-

{strings.getSaveGroupDescription()}

-
+
+ +

{strings.getUngroupDescription()}

+

{strings.getSaveGroupDescription()}

+
+
); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx index 2de3a805c95e9b..999c1c2daaf5b5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx @@ -11,8 +11,10 @@ import { ComponentStrings } from '../../../i18n'; const { MultiElementSettings: strings } = ComponentStrings; export const MultiElementSettings: FunctionComponent = () => ( - -

{strings.getMultipleElementsDescription()}

-

{strings.getMultipleElementsActionsDescription()}

-
+
+ +

{strings.getMultipleElementsDescription()}

+

{strings.getMultipleElementsActionsDescription()}

+
+
); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss index f9ce6f3cfb555d..338d515165e43c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss @@ -2,7 +2,6 @@ @include euiScrollBar; width: 100%; - padding: $euiSizeM; max-height: 100vh; overflow-y: auto; overflow-x: hidden; @@ -25,6 +24,15 @@ margin-bottom: $euiSizeS; } +.canvasSidebar__panel { + border-bottom: $euiBorderThin; + padding: $euiSizeS $euiSizeM; + + &--isEmpty { + border-bottom: none; + } +} + .canvasSidebar__panel-noMinWidth .euiButton { min-width: 0; } diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js index 29ca72a9737a1f..bf149a6c2acb89 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js @@ -7,10 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiPanel } from '@elastic/eui'; - export const SidebarSection = ({ children }) => ( - {children} +
{children}
); SidebarSection.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js index 192786ae86a450..8e1522eae8dcc3 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js @@ -10,7 +10,7 @@ import { EuiTitle, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; export const SidebarSectionTitle = ({ title, tip, children }) => { const formattedTitle = ( - +

{title}

); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss index 24453fcf0411ed..92b0c50a6be4f5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss @@ -1,3 +1,7 @@ .canvasLayout__sidebarHeader { - padding: ($euiSizeXS * .5) 0; + padding: calc(#{$euiSizeM} + 2px) $euiSizeS; } + +.canvasLayout__sidebarHeaderWorkpad { + padding: calc(#{$euiSizeS} * .75) 0; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js index 9693540769d50a..1a441814750912 100644 --- a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js +++ b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js @@ -105,22 +105,30 @@ export const TextStylePicker = ({ return (
+ + doChange('family', value)} /> + doChange('size', Number(e.target.value))} options={fontSizes.map(size => ({ text: String(size), value: size }))} + prepend="Size" /> - - doChange('family', value)} /> - - + - + + + doChange('color', value)} + colors={colors} + /> + @@ -138,13 +147,7 @@ export const TextStylePicker = ({ isIconOnly idSelected={align} onChange={onAlignmentChange} - /> - - - doChange('color', value)} - colors={colors} + className="canvasSidebar__buttonGroup" /> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js b/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js index ec7386bddace6a..7dfc378432b573 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js @@ -67,9 +67,11 @@ export class WorkpadConfig extends PureComponent { return (
- -

{strings.getTitle()}

-
+
+ +

{strings.getTitle()}

+
+
@@ -129,37 +131,38 @@ export class WorkpadConfig extends PureComponent {
- - - - {strings.getGlobalCSSLabel()} - - - } - > -
- this.setState({ css: e.target.value })} - rows={10} - /> - - setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}> - {strings.getApplyStylesheetButtonLabel()} - - -
-
+
+ + + {strings.getGlobalCSSLabel()} + + + } + > +
+ this.setState({ css: e.target.value })} + rows={10} + /> + + setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}> + {strings.getApplyStylesheetButtonLabel()} + + +
+
+
); } diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot index 5525df639be016..ffe87129c76fae 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot @@ -12,7 +12,7 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` >
- Series Identifier + Series id
- Select Series + Select series