diff --git a/.ci/jobs.yml b/.ci/jobs.yml index 1740e1db33f2d0d..f9302db4b29108b 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -1,7 +1,26 @@ JOB: - - selenium - - intake - - x-pack + - kibana-intake + - x-pack-intake + # make sure all kibana-ciGRoups are listed in tasks/function_test_groups.js + - kibana-ciGroup1 + - kibana-ciGroup2 + - kibana-ciGroup3 + - kibana-ciGroup4 + - kibana-ciGroup5 + - kibana-ciGroup6 + - kibana-ciGroup7 + - kibana-ciGroup8 + - kibana-ciGroup9 + - kibana-ciGroup10 + - kibana-ciGroup11 + - kibana-ciGroup12 + # make sure all x-pack-ciGroups are listed in test/scripts/jenkins_xpack_ci_group.sh + - x-pack-ciGroup1 + - x-pack-ciGroup2 + - x-pack-ciGroup3 + - x-pack-ciGroup4 + - x-pack-ciGroup5 + - x-pack-ciGroup6 # `~` is yaml for `null` exclude: ~ diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh new file mode 100755 index 000000000000000..85cefec469a75c4 --- /dev/null +++ b/.ci/packer_cache.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# run setup script that gives us node, yarn, and bootstraps the project +source "src/dev/ci_setup/setup.sh"; + +# cache es snapshots +node scripts/es snapshot --download-only; + +# archive cacheable directories +mkdir -p "$HOME/.kibana/bootstrap_cache" +tar -cf "$HOME/.kibana/bootstrap_cache/master.tar" \ + node_modules \ + packages/*/node_modules \ + x-pack/node_modules \ + x-pack/plugins/*/node_modules \ + .es; diff --git a/.ci/run.sh b/.ci/run.sh index 32c138bd2f45086..1761c5e78cdcf91 100755 --- a/.ci/run.sh +++ b/.ci/run.sh @@ -5,16 +5,23 @@ set -e # move to Kibana root cd "$(dirname "$0")/.." +./src/dev/ci_setup/load_bootstrap_cache.sh; + case "$JOB" in -"selenium") - ./test/scripts/jenkins_selenium.sh - ;; -"intake") +kibana-intake) ./test/scripts/jenkins_unit.sh ;; -"x-pack") +kibana-ciGroup*) + export CI_GROUP="${JOB##kibana-ciGroup}" + ./test/scripts/jenkins_ci_group.sh + ;; +x-pack-intake) ./test/scripts/jenkins_xpack.sh ;; +x-pack-ciGroup*) + export CI_GROUP="${JOB##x-pack-ciGroup}" + ./test/scripts/jenkins_xpack_ci_group.sh + ;; *) echo "JOB '$JOB' is not implemented." exit 1 diff --git a/.eslintignore b/.eslintignore index f697ad004caab34..50901af5b9eeac7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -27,7 +27,8 @@ bower_components /x-pack/coverage /x-pack/build /x-pack/plugins/**/__tests__/fixtures/** -/x-pack/plugins/canvas/common/lib/grammar.js +/packages/kbn-interpreter/common/lib/grammar.js +/packages/kbn-interpreter/plugin /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts **/*.js.snap diff --git a/.gitignore b/.gitignore index 15fa517ae7474d7..ca3da4ab034e4b4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ package-lock.json npm-debug.log* .tern-project **/public/index.css +/packages/kbn-interpreter/plugin diff --git a/.i18nrc.json b/.i18nrc.json index 68fa80b55fe6aa6..33e25169ce8275b 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -16,6 +16,7 @@ "tagCloud": "src/core_plugins/tagcloud", "xpack.grokDebugger": "x-pack/plugins/grokdebugger", "xpack.idxMgmt": "x-pack/plugins/index_management", + "xpack.infra": "x-pack/plugins/infra", "xpack.licenseMgmt": "x-pack/plugins/license_management", "xpack.monitoring": "x-pack/plugins/monitoring", "xpack.rollupJobs": "x-pack/plugins/rollup", @@ -26,7 +27,8 @@ "exclude": [ "src/ui/ui_render/bootstrap/app_bootstrap.js", "src/ui/ui_render/ui_render_mixin.js", - "x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js", - "x-pack/plugins/monitoring/public/directives/alerts/index.js" + "x-pack/plugins/infra/public/utils/loading_state/loading_result.ts", + "x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts" + ] } diff --git a/README.md b/README.md index 2369a52cbe6518a..d48765f1f60dad1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Kibana 7.0.0-alpha1 +# Kibana Kibana is your window into the [Elastic Stack](https://www.elastic.co/products). Specifically, it's a browser-based analytics and search dashboard for Elasticsearch. @@ -37,16 +37,6 @@ out an open PR: - For all other questions, check out the [FAQ.md](FAQ.md) and [wiki](https://github.com/elastic/kibana/wiki). -### Snapshot Builds - -For the daring, snapshot builds are available. These builds are created nightly and have undergone no formal QA, so they should never be run in production. All builds are 64 bit. - -| platform | default | OSS | -| --- | --- | --- | -| OSX | [tar](https://snapshots.elastic.co/downloads/kibana/kibana-7.0.0-alpha1-SNAPSHOT-darwin-x86_64.tar.gz) | [tar](https://snapshots.elastic.co/downloads/kibana/kibana-oss-7.0.0-alpha1-SNAPSHOT-darwin-x86_64.tar.gz) | -| Linux | [tar](https://snapshots.elastic.co/downloads/kibana/kibana-7.0.0-alpha1-SNAPSHOT-linux-x86_64.tar.gz) [deb](https://snapshots.elastic.co/downloads/kibana/kibana-7.0.0-alpha1-SNAPSHOT-amd64.deb) [rpm](https://snapshots.elastic.co/downloads/kibana/kibana-7.0.0-alpha1-SNAPSHOT-x86_64.rpm) | [tar](https://snapshots.elastic.co/downloads/kibana/kibana-oss-7.0.0-alpha1-SNAPSHOT-linux-x86_64.tar.gz) [deb](https://snapshots.elastic.co/downloads/kibana/kibana-oss-7.0.0-alpha1-SNAPSHOT-amd64.deb) [rpm](https://snapshots.elastic.co/downloads/kibana/kibana-oss-7.0.0-alpha1-SNAPSHOT-x86_64.rpm) | -| Windows | [zip](https://snapshots.elastic.co/downloads/kibana/kibana-7.0.0-alpha1-SNAPSHOT-windows-x86_64.zip) | [zip](https://snapshots.elastic.co/downloads/kibana/kibana-oss-7.0.0-alpha1-SNAPSHOT-windows-x86_64.zip) | - ## Documentation Visit [Elastic.co](http://www.elastic.co/guide/en/kibana/current/index.html) for the full Kibana documentation. diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index f3ac819f430f31e..5f3f289cd0d7d45 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -13,7 +13,7 @@ This section summarizes the changes in each release. -* <> +* <> -- @@ -44,3 +44,170 @@ This section summarizes the changes in each release. //[float] //=== Known Issues + +[[release-notes-7.0.0-alpha1]] +== {kib} 7.0.0-alpha1 + +[float] +[[breaking-7.0.0]] +=== Breaking changes + +For more details about breaking changes in this release, see +<>. + +Discover:: +* Does not apply `query:queryString:options` to `query_string` filters {pull}15640[#15640] +* Removes `default_field` from `query:queryString:options` {pull}18966[#18966] + +Monitoring:: +* Removes `node_resolver` setting {pull}21181[#21181] + +Operations:: +* Removes tribe node support {pull}16397[#16397] +* Creates separate startup scripts for development and production {pull}13806[#13806] +* Sets default port based on protocol {pull}21564[#21564] +* Removes deprecated `/shorten` API {pull}21861[#21861] + +[float] +[[deprecation-7.0.0]] +=== Deprecations + +Geo:: +* Fixes legacy tilemap loading {pull}22095[#22095] + +[float] +[[K7-design-7.0.0]] +=== K7 UI Design + +{kib} 7.0.0-alpha1 includes a new design for {kib} called K7. In this early stage, +K7 is still a little rough around the edges. If you'd like to switch back to the +existing K6 design, go to *Management > Advanced Settings* and turn +off the *k7design* setting. The option to switch to the old design +will be removed before 7.0.0 GA. + +[float] +[[enhancement-7.0.0]] +=== Enhancements + +Machine Learning:: +* Updates job type and APM module icon to new designs {pull}25380[#25380] +* Allows model plot enablement via checkbox in MultiMetric/Population Job creation {pull}24914[#24914] +* Adds support for the rare detector for charts in Anomaly Explorer and Singe Metric viewer {pull}21524[#21524] + +Reporting:: +* Adds png output to reports {pull}24759[#24759] +* Sorts ascending on sort order first then ascending on name. Any menu item +without a sort order gets set to zero. {pull}25058[#25058] + +Visualizations:: +* Adds a console.error for visualize errors {pull}24581[#24581] + +[float] +[[bug-7.0.0]] +=== Bug fixes + +APM:: +* Overrides EUI chart default styles for gridlines {pull}21723[#21723] +* Adds section titles to span detail modal {pull}20717[#20717] + +Canvas:: +* Fixes duplicate `Value` options in math select value {pull}25556[#25556] +* Gets correct plugins path {pull}25448[#25448] +* Quotes the index pattern in SQL input {pull}25488[#25488] +* Decreases the size of tray toggle {pull}25470[#25470] +* Improves the plugin pre-build {pull}25267[#25267] + +Dashboard:: +* Removes `dashboardContext` function and makes Timelion, Vega, and Time Series +Visual Builder use `buildEsQuery` {pull}23227[#23227] + +Design:: +* Converts Security UI from LESS to Sass {pull}25079[#25079] +* Adds boilerplate Sass for Kibana core {pull}21185[#21185] + +Discover:: +* Adds debug code to flaky field_data test {pull}15535[#15535] +* Gets even more debug info for flaky field_data test {pull}17627[#17627] + +Geo:: +* Fixes feature/align map config settings {pull}19450[#19450] + +Kibana App:: + +* Adds warning to the `documentation_links` file about link validation gotcha {pull}24786[#24786] +* Adds workaround for `getDerivedStateFromProps` change in react 16.4 {pull}25142[#25142] + +Kibana Home & Add Data:: +* Fixes "Set up index patterns" link on home page {pull}16128[#16128] + +Machine Learning:: +* Shows useful error on invalid query in JobList search bar {pull}25153[#25153] +* Adds user privilege check to Jobs List group selector control {pull}25225[#25225] +* Fixes file data viz file size check and formats as bytes {pull}25295[#25295] +* Fixes the layout of the cards in the Data Visualizer on IE {pull}25383[#25383] +* Adds better error reporting for reading and importing data {pull}24269[#24269] +* Displays an ordinal y axis for low cardinality rare charts {pull}24852[#24852] +* Fixes typo in job validation message {pull}25130[#25130] +* Removes deprecated `angularjs` based jobs list and related code {pull}25216[#25216] + +Management:: +* Adds boilerplate for remote clusters management app {pull}25369[#25369] +* Adds `ignore_failure` to ingest common auto complete in console {pull}24915[#24915] +* Removes support for expression-based scripted fields {pull}14310[#14310] +* Adds WatchErrors to capture invalid watches {pull}23887[#23887] +* Rewords the translation id for error with missing property in Watcher {pull}24753[#24753] + +Monitoring:: +* Renames Monitoring `FormattedMessage` to `FormattedAlert` {pull}24197[#24197] +* Uses the cluster name from metadata if it exists {pull}24495[#24495] + +Operations:: +* Removes node fallback from kibana-keystore {pull}15066[#15066] +* Adds debug script to set inspect flags {pull}15967[#15967] +* Uses snake case for scripts/kibana-keystore.js and scripts/kibana-plugin.js {pull}15331[#15331] +* Updates license info in package.json {pull}20353[#20353] +* Fixes error log formatting {pull}24788[#24788] +* Matches chalk dependency version on Kibana with the one used on X-Pack {pull}20621[#20621] +* Fixes non-conforming licenses on devDependencies and adds the ability to whitelist devOnly licenses {pull}23859[#23859] +* Adds jsxa11y into eslint rules {pull}23932[#23932] +* Reverts Bump react-grid-layout to 0.16.0 {pull}14912[#14912] +* Reverts breaking change for Status API {pull}21927[#21927] +* Converts `utils/collection` to TypeScript {pull}23992[#23992] +* Removes usage of update_all_types {pull}16406[#16406] +* Improves the `yarn kbn bootstrap` speed by using yarn workspaces for packages inside `packages/*` and `x-pack` {pull}24095[#24095] +* Runs jenkins:unit task with dev flag in order to run license check {pull}19832[#19832] +* Does not break on startup in debug mode {pull}19219[#19219] + + +Platform:: +* Transforms plugin deprecations before checking for unused settings {pull}21294[#21294] +* Expands list of restricted globals in `eslint-config-kibana` {pull}15798[#15798] +* Makes logs easier to read on Windows with chalk colors {pull}15557[#15557] + +Querying & Filtering:: +* Fixes wildcard queries against the default field {pull}24778[#24778] + +Reporting:: +* Returns promise in Reporting jobs API {pull}24769[#24769] + +Security:: +* Implements the K7 login screen {pull}23512[#23512] + +Sharing:: + +* Fixes issue with debounce function running after component was unmounted {pull}15045[#15045] + +Visualizations:: +* Defaults the scroll wheel zoom to false on Vega maps {pull}21169[#21169] +* Fixes problem within the input_vis_control plugin that prevents it from updating correctly +if the field is switched, and then switched back to the previous field {pull}25164[#25164] +* Uses `vega-nocanvas` instead of vega lib {pull}16137[#16137] +* Migrates visualization from Angular to React {pull}16425[#16425] +* Fixes maps for reporting (#15272) {pull}15358[#15358] +* Stops creation of nested search source per postflightrequest {pull}20373[#20373] +* Moves inspector code from Vis to embeddable visualize handler {pull}24112[#24112] +* Removes inspector from Vis {pull}24112[#24112] + + + + diff --git a/docs/development/visualize/development-create-visualization.asciidoc b/docs/development/visualize/development-create-visualization.asciidoc index 56ed620e148c591..94e06df60ed6ff5 100644 --- a/docs/development/visualize/development-create-visualization.asciidoc +++ b/docs/development/visualize/development-create-visualization.asciidoc @@ -71,8 +71,8 @@ The list of common parameters: - *options.showQueryBar*: show or hide query bar (defaults to true) - *options.showFilterBar*: show or hide filter bar (defaults to true) - *options.showIndexSelection*: show or hide index selection (defaults to true) -- *stage*: Set this to "experimental" or "labs" to mark your visualization as experimental. -Labs visualizations can also be disabled from the advanced settings. (defaults to "production") +- *stage*: Set this to "experimental" to mark your visualization as experimental. +Experimental visualizations can also be disabled from the advanced settings. (defaults to "production") - *feedbackMessage*: You can provide a message (which can contain HTML), that will be appended to the experimental notification in visualize, if your visualization is experimental or in lab mode. diff --git a/docs/development/visualize/development-embedding-visualizations.asciidoc b/docs/development/visualize/development-embedding-visualizations.asciidoc index ecfa93ba9e5b2a0..4748f3b546b1315 100644 --- a/docs/development/visualize/development-embedding-visualizations.asciidoc +++ b/docs/development/visualize/development-embedding-visualizations.asciidoc @@ -55,4 +55,4 @@ The returned `EmbeddedVisualizeHandler` itself has the following methods and pro - `removeRenderCompleteListener(listener)`: removes an event listener from the handler again You can find the detailed `EmbeddedVisualizeHandler` documentation in its -{repo}blob/{branch}/src/ui/public/visualize/loader/embedded_visualize_handler.js[source code]. \ No newline at end of file +{repo}blob/{branch}/src/ui/public/visualize/loader/embedded_visualize_handler.ts[source code]. \ No newline at end of file diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 8b2a8e2eee56191..59c945ee9e55d9b 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -44,6 +44,10 @@ include::canvas.asciidoc[] include::ml/index.asciidoc[] +include::infrastructure/index.asciidoc[] + +include::logs/index.asciidoc[] + include::apm/index.asciidoc[] include::graph/index.asciidoc[] diff --git a/docs/infrastructure/images/infra-sysmon.jpg b/docs/infrastructure/images/infra-sysmon.jpg new file mode 100644 index 000000000000000..36afcb82061cc95 Binary files /dev/null and b/docs/infrastructure/images/infra-sysmon.jpg differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc new file mode 100644 index 000000000000000..1167cfcf6463523 --- /dev/null +++ b/docs/infrastructure/index.asciidoc @@ -0,0 +1,26 @@ +[role="xpack"] +[[xpack-infra]] += Infrastructure + +[partintro] +-- +beta[] + +Use the interactive Infrastructure UI to monitor your infrastructure and +identify problems in real time. You can explore metrics and logs for common +servers, containers, and services. + +[role="screenshot"] +image::infrastructure/images/infra-sysmon.jpg[Infrastructure Overview in Kibana] + + +[float] +== Add data sources +Kibana provides step-by-step instructions to help you add your data sources. +The {infra-guide}[Infrastructure Monitoring Guide] is good source for more detailed +instructions and information. + +-- + +include::monitor.asciidoc[] +include::infra-ui.asciidoc[] diff --git a/docs/infrastructure/infra-ui.asciidoc b/docs/infrastructure/infra-ui.asciidoc new file mode 100644 index 000000000000000..1e7621627a14733 --- /dev/null +++ b/docs/infrastructure/infra-ui.asciidoc @@ -0,0 +1,62 @@ +[role="xpack"] +[[infra-ui]] +== Using the Infrastructure UI + +Use the Infrastructure UI in {kib} to monitor your infrastructure and identify +problems in real time. + +You start with an overview of your infrastructure. +Then you can use the interactive UI to drill down into areas of interest. + +[role="screenshot"] +image::infrastructure/images/infra-sysmon.jpg[Infrastructure Overview in Kibana] + +TIP: Right-click an element to jump to related logs or metrics. + +[float] +[[infra-cat]] +=== View your infrastructure by hosts or container + +Select the high-level view: *Hosts*, *Kubernetes*, or *Docker*. +When you change views, you can see the same data through the perspective of a +different category. + +[float] +[[infra-search]] +=== Use the power of Search + +The Search bar is always available. You can use it to perform adhoc or structured searches. +The Search feature can surface the information you're looking for, even if the +other options you have selected would normally filter it out. + +[float] +[[infra-metric]] +=== Start at a high-level by selecting the metric + +This filter helps you start focusing on potential problem areas that may need +further investigation. You'll see metrics that are most relevant for hosts or +the container you selected. + +[float] +[[infra-group]] +=== Group components + +The *Group By* selector offers grouping options that are native and specific for +your physical, virtual, and container-based infrastructure. +Examples include Availability Zone, Machine Type, Project ID, and Cloud Provider +for Hosts, and Namespace and Node for Kubernetes. + +[float] +[[infra-date]] +=== Specify the date range + +Use the time selector to focus on a specific timeframe. + +[float] +[[infra-refresh]] +=== Auto-refresh or pause + +Set auto-refresh to keep up-to-date information coming in, or stop +refreshing to focus on historical data without new distractions. + + diff --git a/docs/infrastructure/monitor.asciidoc b/docs/infrastructure/monitor.asciidoc new file mode 100644 index 000000000000000..90b27460e89e450 --- /dev/null +++ b/docs/infrastructure/monitor.asciidoc @@ -0,0 +1,35 @@ +[role="xpack"] +[[monitor-infra]] +== Monitor your infrastructure + +You start with a visual summary of your infrastructure. +Then you can drill down into areas of interest. + +[float] +=== Monitor hosts and containers + +View your infrastructure by hosts or containers. The UI helps you group, filter, +and target data to help you find what you seek. + +[float] +=== View metrics + +View relevant metric information such as CPU usage, memory usage, load, and inbound or +outbound traffic. + +[float] +=== View logs + +View real-time and historical logs in the same viewer, streaming or pausing as needed. +When you stream logs, the most recent event appears at the bottom of console. +You can easily correlate metrics and logs. Right-click an element in the +Infrastructure UI, and select *View logs*. + + + + + + + + + diff --git a/docs/logs/images/logs-console.png b/docs/logs/images/logs-console.png new file mode 100644 index 000000000000000..ccc088144d8e2e0 Binary files /dev/null and b/docs/logs/images/logs-console.png differ diff --git a/docs/logs/index.asciidoc b/docs/logs/index.asciidoc new file mode 100644 index 000000000000000..1356ff522c22a7b --- /dev/null +++ b/docs/logs/index.asciidoc @@ -0,0 +1,26 @@ +[role="xpack"] +[[xpack-logs]] += Logs + +[partintro] +-- + +beta[] +Use the Logs UI to explore logs for common servers, containers, and services. +{kib} provides a compact, console-like display that you can customize. + +[role="screenshot"] +image::logs/images/logs-console.png[Log Console in Kibana] + + +[float] +== Add data sources + +Kibana provides step-by-step instructions to help you add your data sources. +The {infra-guide}[Infrastructure Monitoring Guide] is good source for more detailed information and +instructions. + + +-- + +include::logs-ui.asciidoc[] \ No newline at end of file diff --git a/docs/logs/logs-ui.asciidoc b/docs/logs/logs-ui.asciidoc new file mode 100644 index 000000000000000..f7050049b0684a0 --- /dev/null +++ b/docs/logs/logs-ui.asciidoc @@ -0,0 +1,37 @@ +[role="xpack"] +[[logs-ui]] +== Using the Logs UI + +Customize the Infrastructure UI to focus on the data you want to see and control how you see it. + +[role="screenshot"] +image::logs/images/logs-console.png[Log Console in Kibana] + +[float] +[[logs-search]] +=== Use the power of Search +The Search bar is always available. Use it to perform adhoc and structured searches. + +[float] +[[logs-time]] +=== Jump to a specific time period +Use the time selector to focus on a specific timeframe. + +[float] +[[logs-customize]] +=== Customize your view +Use *Customize* to adjust your console view and to set the time scale of the log data. + +* *Text size.* Select `Small`, `Medium`, or `Large`. +* *Wrap long lines.* Enable or disable line wrap. +* *Minimap Scale.* Set the scale to 'year', 'month', 'week', 'day', 'hour', or 'minute'. + +[float] +[[logs-stream]] +=== Stream or pause logs +You can stream data for live log tailing, or pause streaming to focus on historical log data. +When you are streaming logs, the most recent log appears at the bottom on the console. +Historical data offers infinite scrolling. + + + \ No newline at end of file diff --git a/docs/reporting/configuring-reporting.asciidoc b/docs/reporting/configuring-reporting.asciidoc index d6a69a1019aa368..67167f0d9f460c1 100644 --- a/docs/reporting/configuring-reporting.asciidoc +++ b/docs/reporting/configuring-reporting.asciidoc @@ -2,12 +2,22 @@ [[configuring-reporting]] == Configuring Reporting -NOTE: If you use a reverse-proxy (NGINX, Apache, etc.) to access Kibana, you need -to configure the <> +You can configure settings in `kibana.yml` to control how {reporting} +communicates with the Kibana server, manages background jobs, and captures +screenshots. See <> for the complete +list of settings. + +=== Encryption Keys for Multiple Kibana Instances By default, a new encryption key is generated for {reporting} each time -you start Kibana. This means any pending reports will fail if you -restart Kibana. +you start Kibana. This means if a static encryption key is not persisted in the +Kibana configuration, any pending reports will fail when you restart Kibana. + +If you are load balancing across multiple Kibana instances, they need to have +the same reporting encryption key. Otherwise, report generation will fail if a +report is queued through one instance and another instance picks up the job +from the report queue. The other instance will not be able to decrypt the +reporting job metadata. To set a static encryption key for reporting, set the `xpack.reporting.encryptionKey` property in the `kibana.yml` @@ -18,14 +28,44 @@ configuration file. You can use any text string as the encryption key. xpack.reporting.encryptionKey: "something_secret" -------------------------------------------------------------------------------- -NOTE: If you are load balancing across multiple Kibana instances, they need -to have the same reporting encryption key. Otherwise, report generation -will fail if a report is requested through one instance and another instance -picks up the job from the report queue. +=== Reporting Indices for Multiple Kibana Workspaces -You can also configure settings in `kibana.yml` to control how {reporting} -communicates with the Kibana server, manages background jobs, and captures -screenshots. See <> for the complete -list of settings. +If you divide workspaces in an Elastic cluster using multiple Kibana instances +with a different `kibana.index` setting per instance, you must set a unique `xpack.reporting.index` +setting per `kibana.index`. Otherwise, report generation will periodically fail +if a report is queued through an instance with one `kibana.index` setting, and +an instance with a different `kibana.index` attempts to claim the job. + +Kibana instance A: +[source,yaml] +-------------------------------------------------------------------------------- +kibana.index: ".kibana-a" +xpack.reporting.index: ".reporting-a" +xpack.reporting.encryptionKey: "something_secret" +-------------------------------------------------------------------------------- + +Kibana instance B: +[source,yaml] +-------------------------------------------------------------------------------- +kibana.index: ".kibana-b" +xpack.reporting.index: ".reporting-b" +xpack.reporting.encryptionKey: "something_secret" +-------------------------------------------------------------------------------- + +NOTE: If security is enabled, the `xpack.reporting.index` setting should begin +with `.reporting-` in order for the `kibana_system` role to have the necessary +privileges over the index. + +=== Using Reverse Proxies + +If your Kibana instance requires a reverse-proxy (NGINX, Apache, etc.) for +access, because of rewrite rules or special headers being added by the proxy, +then you need to configure the `xpack.reporting.kibanaServer` settings to make +the headless browser process connect to the proxy in <>. + +NOTE: A headless browser runs on the Kibana server to open a Kibana page for +capturing screenshots. Configuring the `xpack.reporting.kibanaServer` settings +to point to a proxy host requires that the Kibana server has network access to +the proxy. include::{kib-repo-dir}/security/reporting.asciidoc[] diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 52d07d03db2e65c..1a84e40069c10b4 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -6,8 +6,11 @@ ++++ By default, the Monitoring application is enabled, but data collection -is disabled. When you first start {kib} monitoring, you will be prompted to -enable data collection. +is disabled. When you first start {kib} monitoring, you are prompted to +enable data collection. If you are using {security}, you must be +signed in as a user with the `cluster:manage` privilege to enable +data collection. The built-in `superuser` role has this privilege and the +built-in `elastic` user has this role. You can adjust how monitoring data is collected from {kib} and displayed in {kib} by configuring settings in the diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 6aafeedf9cba4c7..4e0792d942914fa 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -18,9 +18,9 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: Set to `false` to disable {reporting}. `xpack.reporting.encryptionKey`:: -Set to any text string. By default, Kibana generates a random key when it starts, -which causes pending reports to fail on restart. Configure this setting to use -the same key across restarts. +Set to any text string. By default, Kibana will generate a random key when it +starts, which will cause pending reports to fail after restart. Configure this +setting to preserve the same key across multiple restarts and multiple instances of Kibana. [float] [[reporting-kibana-server-settings]] @@ -39,6 +39,9 @@ The protocol for accessing Kibana, typically `http` or `https`. `xpack.reporting.kibanaServer.hostname`:: The hostname for accessing Kibana, if different from the `server.name` value. +NOTE: Configuring the `xpack.reporting.kibanaServer` settings to point to a +proxy host requires that the Kibana server has network access to the proxy. + [float] [[reporting-job-queue-settings]] ==== Background Job Settings @@ -131,9 +134,10 @@ Defaults to `10485760` (10mB) ==== Advanced Settings `xpack.reporting.index`:: -Reporting uses a weekly index in Elasticsearch to store the reporting job and the report -content. The index is automatically created if it does not already exist. -Defaults to `.reporting` +Reporting uses a weekly index in Elasticsearch to store the reporting job and +the report content. The index is automatically created if it does not already +exist. Configure this to a unique value, beginning with `.reporting-`, for every +Kibana instance that has a unique `kibana.index` setting. Defaults to `.reporting` `xpack.reporting.roles.allow`:: Specifies the roles in addition to superusers that can use reporting. diff --git a/package.json b/package.json index e13e9694913d0c6..e873a7ffa180abb 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "uiFramework:documentComponent": "cd packages/kbn-ui-framework && yarn documentComponent", "kbn:watch": "node scripts/kibana --dev --logging.json=false", "build:types": "tsc --p tsconfig.types.json", - "kbn:bootstrap": "yarn build:types" + "kbn:bootstrap": "yarn build:types && node scripts/register_git_hook" }, "repository": { "type": "git", @@ -92,6 +92,7 @@ "@kbn/pm": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", + "@kbn/interpreter": "1.0.0", "JSONStream": "1.1.1", "abortcontroller-polyfill": "^1.1.9", "angular": "1.6.9", @@ -145,7 +146,6 @@ "js-yaml": "3.4.1", "json-stringify-pretty-compact": "1.0.4", "json-stringify-safe": "5.0.1", - "jstimezonedetect": "1.0.5", "leaflet": "1.0.3", "leaflet-draw": "0.4.10", "leaflet-responsive-popup": "0.2.0", @@ -333,7 +333,6 @@ "gulp-babel": "^7.0.1", "gulp-sourcemaps": "1.7.3", "has-ansi": "^3.0.0", - "husky": "^0.14.3", "image-diff": "1.6.0", "intl-messageformat-parser": "^1.4.0", "istanbul-instrumenter-loader": "3.0.0", diff --git a/packages/eslint-config-kibana/.eslintrc.js b/packages/eslint-config-kibana/.eslintrc.js index d2d1efa9180fafc..91c850b5117f21a 100644 --- a/packages/eslint-config-kibana/.eslintrc.js +++ b/packages/eslint-config-kibana/.eslintrc.js @@ -1,4 +1,7 @@ -const RESTRICTED_GLOBALS = require('./restricted_globals') +const semver = require('semver'); + +const PKG = require('../../package.json'); +const RESTRICTED_GLOBALS = require('./restricted_globals'); module.exports = { parser: 'babel-eslint', @@ -12,7 +15,13 @@ module.exports = { 'prefer-object-spread', 'jsx-a11y', ], - + + settings: { + react: { + version: semver.coerce(PKG.dependencies.react), + }, + }, + env: { es6: true, node: true, diff --git a/packages/kbn-dev-utils/index.d.ts b/packages/kbn-dev-utils/index.d.ts index 9d8fd8889b7e5df..dceadb9496f3bfe 100644 --- a/packages/kbn-dev-utils/index.d.ts +++ b/packages/kbn-dev-utils/index.d.ts @@ -18,3 +18,4 @@ */ export * from './src/tooling_log'; +export * from './src/serializers'; diff --git a/packages/kbn-dev-utils/src/index.js b/packages/kbn-dev-utils/src/index.js index 4e2d1b62a92f715..82492e568f4b5bd 100644 --- a/packages/kbn-dev-utils/src/index.js +++ b/packages/kbn-dev-utils/src/index.js @@ -19,3 +19,4 @@ export { withProcRunner } from './proc_runner'; export { ToolingLog, ToolingLogTextWriter, pickLevelFromFlags } from './tooling_log'; +export { createAbsolutePathSerializer } from './serializers'; diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.d.ts b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.d.ts new file mode 100644 index 000000000000000..66e94571a230001 --- /dev/null +++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function createAbsolutePathSerializer( + rootPath: string +): { print(...args: any[]): string; test(value: any): boolean }; diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.js b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.js new file mode 100644 index 000000000000000..d944772048eb93b --- /dev/null +++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function createAbsolutePathSerializer(rootPath) { + return { + print: value => value.replace(rootPath, '').replace(/\\/g, '/'), + test: value => typeof value === 'string' && value.startsWith(rootPath), + }; +} diff --git a/packages/kbn-dev-utils/src/serializers/index.d.ts b/packages/kbn-dev-utils/src/serializers/index.d.ts new file mode 100644 index 000000000000000..3b49e243058df72 --- /dev/null +++ b/packages/kbn-dev-utils/src/serializers/index.d.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 { createAbsolutePathSerializer } from './absolute_path_serializer'; diff --git a/packages/kbn-dev-utils/src/serializers/index.js b/packages/kbn-dev-utils/src/serializers/index.js new file mode 100644 index 000000000000000..3b49e243058df72 --- /dev/null +++ b/packages/kbn-dev-utils/src/serializers/index.js @@ -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 { createAbsolutePathSerializer } from './absolute_path_serializer'; diff --git a/packages/kbn-es/src/cli_commands/snapshot.js b/packages/kbn-es/src/cli_commands/snapshot.js index 0a585e92cfd74bf..c01a4fa08ec590b 100644 --- a/packages/kbn-es/src/cli_commands/snapshot.js +++ b/packages/kbn-es/src/cli_commands/snapshot.js @@ -35,6 +35,7 @@ exports.help = (defaults = {}) => { --install-path Installation path, defaults to 'source' within base-path --password Sets password for elastic user [default: ${password}] -E Additional key=value settings to pass to Elasticsearch + --download-only Download the snapshot but don't actually start it Example: @@ -51,10 +52,16 @@ exports.run = async (defaults = {}) => { esArgs: 'E', }, + boolean: ['download-only'], + default: defaults, }); const cluster = new Cluster(); - const { installPath } = await cluster.installSnapshot(options); - await cluster.run(installPath, { esArgs: options.esArgs }); + if (options['download-only']) { + await cluster.downloadSnapshot(options); + } else { + const { installPath } = await cluster.installSnapshot(options); + await cluster.run(installPath, { esArgs: options.esArgs }); + } }; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index f14be3582f71a61..50f3c5db2d8ca46 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -19,7 +19,7 @@ const execa = require('execa'); const chalk = require('chalk'); -const { installSnapshot, installSource, installArchive } = require('./install'); +const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install'); const { ES_BIN } = require('./paths'); const { log: defaultLog, parseEsLog, extractConfigFiles } = require('./utils'); const { createCliError } = require('./errors'); @@ -50,6 +50,28 @@ exports.Cluster = class Cluster { return { installPath }; } + /** + * Download ES from a snapshot + * + * @param {Object} options + * @property {Array} options.installPath + * @property {Array} options.sourcePath + * @returns {Promise<{installPath}>} + */ + async downloadSnapshot(options = {}) { + this._log.info(chalk.bold('Downloading snapshot')); + this._log.indent(4); + + const { installPath } = await downloadSnapshot({ + log: this._log, + ...options, + }); + + this._log.indent(-4); + + return { installPath }; + } + /** * Download and installs ES from a snapshot * diff --git a/packages/kbn-es/src/install/index.js b/packages/kbn-es/src/install/index.js index 69de1004e4e7224..e2b3f692b22037a 100644 --- a/packages/kbn-es/src/install/index.js +++ b/packages/kbn-es/src/install/index.js @@ -19,4 +19,5 @@ exports.installArchive = require('./archive').installArchive; exports.installSnapshot = require('./snapshot').installSnapshot; +exports.downloadSnapshot = require('./snapshot').downloadSnapshot; exports.installSource = require('./source').installSource; diff --git a/packages/kbn-es/src/install/snapshot.js b/packages/kbn-es/src/install/snapshot.js index de1a635b7f9f6e9..42c1307a0461420 100644 --- a/packages/kbn-es/src/install/snapshot.js +++ b/packages/kbn-es/src/install/snapshot.js @@ -28,26 +28,23 @@ const { installArchive } = require('./archive'); const { log: defaultLog, cache } = require('../utils'); /** - * Installs ES from snapshot + * Download an ES snapshot * * @param {Object} options * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.password * @property {String} options.version * @property {String} options.basePath * @property {String} options.installPath * @property {ToolingLog} options.log */ -exports.installSnapshot = async function installSnapshot({ +exports.downloadSnapshot = async function installSnapshot({ license = 'basic', - password = 'password', version, basePath = BASE_PATH, installPath = path.resolve(basePath, version), log = defaultLog, }) { - // TODO: remove -alpha1 once elastic/elasticsearch#35172 has been merged - const fileName = getFilename(license, version + '-alpha1'); + const fileName = getFilename(license, version); const url = `https://snapshots.elastic.co/downloads/elasticsearch/${fileName}`; const dest = path.resolve(basePath, 'cache', fileName); @@ -56,7 +53,40 @@ exports.installSnapshot = async function installSnapshot({ log.info('license: %s', chalk.bold(license)); await downloadFile(url, dest, log); - return await installArchive(dest, { + + return { + downloadPath: dest, + }; +}; + +/** + * Installs ES from snapshot + * + * @param {Object} options + * @property {('oss'|'basic'|'trial')} options.license + * @property {String} options.password + * @property {String} options.version + * @property {String} options.basePath + * @property {String} options.installPath + * @property {ToolingLog} options.log + */ +exports.installSnapshot = async function installSnapshot({ + license = 'basic', + password = 'password', + version, + basePath = BASE_PATH, + installPath = path.resolve(basePath, version), + log = defaultLog, +}) { + const { downloadPath } = await exports.downloadSnapshot({ + license, + version, + basePath, + installPath, + log, + }); + + return await installArchive(downloadPath, { license, password, basePath, diff --git a/packages/kbn-interpreter/.babelrc b/packages/kbn-interpreter/.babelrc new file mode 100644 index 000000000000000..dc6a77bbe0bcd21 --- /dev/null +++ b/packages/kbn-interpreter/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/webpack_preset"] +} diff --git a/x-pack/plugins/canvas/common/interpreter/cast.js b/packages/kbn-interpreter/common/interpreter/cast.js similarity index 53% rename from x-pack/plugins/canvas/common/interpreter/cast.js rename to packages/kbn-interpreter/common/interpreter/cast.js index 7e559afcba40ed9..cc257a7dc55e030 100644 --- a/x-pack/plugins/canvas/common/interpreter/cast.js +++ b/packages/kbn-interpreter/common/interpreter/cast.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 { getType } from '../lib/get_type'; @@ -19,8 +32,9 @@ export function castProvider(types) { for (let i = 0; i < toTypeNames.length; i++) { // First check if the current type can cast to this type - if (fromTypeDef && fromTypeDef.castsTo(toTypeNames[i])) + if (fromTypeDef && fromTypeDef.castsTo(toTypeNames[i])) { return fromTypeDef.to(node, toTypeNames[i], types); + } // If that isn't possible, check if this type can cast from the current type const toTypeDef = types[toTypeNames[i]]; diff --git a/packages/kbn-interpreter/common/interpreter/create_error.js b/packages/kbn-interpreter/common/interpreter/create_error.js new file mode 100644 index 000000000000000..2740358b1c9605d --- /dev/null +++ b/packages/kbn-interpreter/common/interpreter/create_error.js @@ -0,0 +1,26 @@ +/* + * 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 const createError = err => ({ + type: 'error', + error: { + stack: process.env.NODE_ENV === 'production' ? undefined : err.stack, + message: typeof err === 'string' ? err : err.message, + }, +}); diff --git a/x-pack/plugins/canvas/common/interpreter/interpret.js b/packages/kbn-interpreter/common/interpreter/interpret.js similarity index 88% rename from x-pack/plugins/canvas/common/interpreter/interpret.js rename to packages/kbn-interpreter/common/interpreter/interpret.js index ff7a2547f236f38..d2a786cd3c85dce 100644 --- a/x-pack/plugins/canvas/common/interpreter/interpret.js +++ b/packages/kbn-interpreter/common/interpreter/interpret.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 clone from 'lodash.clone'; @@ -112,8 +125,9 @@ export function interpretProvider(config) { (argAsts, argAst, argName) => { const argDef = getByAlias(argDefs, argName); // TODO: Implement a system to allow for undeclared arguments - if (!argDef) + if (!argDef) { throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`); + } argAsts[argDef.name] = (argAsts[argDef.name] || []).concat(argAst); return argAsts; @@ -142,8 +156,9 @@ export function interpretProvider(config) { const argAstsWithDefaults = reduce( argDefs, (argAsts, argDef, argName) => { - if (typeof argAsts[argName] === 'undefined' && typeof argDef.default !== 'undefined') + if (typeof argAsts[argName] === 'undefined' && typeof argDef.default !== 'undefined') { argAsts[argName] = [fromExpression(argDef.default, 'argument')]; + } return argAsts; }, diff --git a/x-pack/plugins/canvas/common/interpreter/socket_interpret.js b/packages/kbn-interpreter/common/interpreter/socket_interpret.js similarity index 72% rename from x-pack/plugins/canvas/common/interpreter/socket_interpret.js rename to packages/kbn-interpreter/common/interpreter/socket_interpret.js index c8d5acf4fdd5265..1ea95e0f5f6f1e2 100644 --- a/x-pack/plugins/canvas/common/interpreter/socket_interpret.js +++ b/packages/kbn-interpreter/common/interpreter/socket_interpret.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 uuid from 'uuid/v4'; @@ -40,8 +53,9 @@ export function socketInterpreterProvider({ // Get the list of functions that are known elsewhere return Promise.resolve(referableFunctions).then(referableFunctionMap => { // Check if the not-found function is in the list of alternatives, if not, throw - if (!getByAlias(referableFunctionMap, functionName)) + if (!getByAlias(referableFunctionMap, functionName)) { throw new Error(`Function not found: ${functionName}`); + } // set a unique message ID so the code knows what response to process const id = uuid(); diff --git a/packages/kbn-interpreter/common/lib/arg.js b/packages/kbn-interpreter/common/lib/arg.js new file mode 100644 index 000000000000000..0aa2b52e35acb0d --- /dev/null +++ b/packages/kbn-interpreter/common/lib/arg.js @@ -0,0 +1,37 @@ +/* + * 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 { includes } from 'lodash'; + +export function Arg(config) { + if (config.name === '_') throw Error('Arg names must not be _. Use it in aliases instead.'); + this.name = config.name; + this.required = config.required || false; + this.help = config.help || ''; + this.types = config.types || []; + this.default = config.default; + this.aliases = config.aliases || []; + this.multi = config.multi == null ? false : config.multi; + this.resolve = config.resolve == null ? true : config.resolve; + this.options = config.options || []; + this.accepts = type => { + if (!this.types.length) return true; + return includes(config.types, type); + }; +} diff --git a/packages/kbn-interpreter/common/lib/arg.test.js b/packages/kbn-interpreter/common/lib/arg.test.js new file mode 100644 index 000000000000000..2edd65cd4af4988 --- /dev/null +++ b/packages/kbn-interpreter/common/lib/arg.test.js @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Arg } from './arg'; + +describe('Arg', () => { + it('sets required to false by default', () => { + const isOptional = new Arg({ + name: 'optional_me', + }); + expect(isOptional.required).toBe(false); + + const isRequired = new Arg({ + name: 'require_me', + required: true, + }); + expect(isRequired.required).toBe(true); + }); +}); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/ast.from_expression.js b/packages/kbn-interpreter/common/lib/ast.from_expression.test.js similarity index 59% rename from x-pack/plugins/canvas/common/lib/__tests__/ast.from_expression.js rename to packages/kbn-interpreter/common/lib/ast.from_expression.test.js index 631973247dc6c46..c144770f94c54fc 100644 --- a/x-pack/plugins/canvas/common/lib/__tests__/ast.from_expression.js +++ b/packages/kbn-interpreter/common/lib/ast.from_expression.test.js @@ -1,35 +1,47 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 expect from 'expect.js'; -import { fromExpression } from '../ast'; -import { getType } from '../../lib/get_type'; +import { fromExpression } from './ast'; +import { getType } from './get_type'; describe('ast fromExpression', () => { describe('invalid expression', () => { it('throws when empty', () => { const check = () => fromExpression(''); - expect(check).to.throwException(/Unable to parse expression/i); + expect(check).toThrowError(/Unable to parse expression/i); }); it('throws with invalid expression', () => { const check = () => fromExpression('wat!'); - expect(check).to.throwException(/Unable to parse expression/i); + expect(check).toThrowError(/Unable to parse expression/i); }); }); describe('single item expression', () => { it('is a chain', () => { const expression = 'whatever'; - expect(fromExpression(expression)).to.have.property('chain'); + expect(fromExpression(expression)).toHaveProperty('chain'); }); it('is a value', () => { const expression = '"hello"'; - expect(fromExpression(expression, 'argument')).to.equal('hello'); + expect(fromExpression(expression, 'argument')).toBe('hello'); }); describe('function without arguments', () => { @@ -44,15 +56,15 @@ describe('ast fromExpression', () => { }); it('is a function ', () => { - expect(getType(block)).to.equal('function'); + expect(getType(block)).toBe('function'); }); it('is csv function', () => { - expect(block.function).to.equal('csv'); + expect(block.function).toBe('csv'); }); it('has no arguments', () => { - expect(block.arguments).to.eql({}); + expect(block.arguments).toEqual({}); }); }); @@ -68,17 +80,17 @@ describe('ast fromExpression', () => { }); it('has arguemnts properties', () => { - expect(block.arguments).not.to.eql({}); + expect(block.arguments).not.toEqual({}); }); it('has index argument with string value', () => { - expect(block.arguments).to.have.property('index'); - expect(block.arguments.index).to.eql(['logstash-*']); + expect(block.arguments).toHaveProperty('index'); + expect(block.arguments.index).toEqual(['logstash-*']); }); it('has oranges argument with string value', () => { - expect(block.arguments).to.have.property('oranges'); - expect(block.arguments.oranges).to.eql(['bananas']); + expect(block.arguments).toHaveProperty('oranges'); + expect(block.arguments.oranges).toEqual(['bananas']); }); }); @@ -94,12 +106,12 @@ describe('ast fromExpression', () => { }); it('is expression type', () => { - expect(block.arguments).to.have.property('exampleFunction'); - expect(block.arguments.exampleFunction[0]).to.have.property('type', 'expression'); + expect(block.arguments).toHaveProperty('exampleFunction'); + expect(block.arguments.exampleFunction[0]).toHaveProperty('type'); }); it('has expected shape', () => { - expect(block.arguments.exampleFunction).to.eql([ + expect(block.arguments.exampleFunction).toEqual([ { type: 'expression', chain: [ @@ -128,12 +140,12 @@ describe('ast fromExpression', () => { }); it('is expression type', () => { - expect(block.arguments).to.have.property('examplePartial'); - expect(block.arguments.examplePartial[0]).to.have.property('type', 'expression'); + expect(block.arguments).toHaveProperty('examplePartial'); + expect(block.arguments.examplePartial[0]).toHaveProperty('type'); }); it('has expected shape', () => { - expect(block.arguments.examplePartial).to.eql([ + expect(block.arguments.examplePartial).toEqual([ { type: 'expression', chain: [ diff --git a/x-pack/plugins/canvas/common/lib/ast.js b/packages/kbn-interpreter/common/lib/ast.js similarity index 81% rename from x-pack/plugins/canvas/common/lib/ast.js rename to packages/kbn-interpreter/common/lib/ast.js index b31848944e9db3e..61cfe94ac955c9d 100644 --- a/x-pack/plugins/canvas/common/lib/ast.js +++ b/packages/kbn-interpreter/common/lib/ast.js @@ -1,10 +1,23 @@ /* - * 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. + * 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 { getType } from '../lib/get_type'; +import { getType } from './get_type'; import { parse } from './grammar'; function getArgumentString(arg, argKey, level = 0) { @@ -48,8 +61,9 @@ function getExpressionArgs(block, level = 0) { const lineLength = acc.split('\n').pop().length; // if arg values are too long, move it to the next line - if (level === 0 && lineLength + argString.length > MAX_LINE_LENGTH) + if (level === 0 && lineLength + argString.length > MAX_LINE_LENGTH) { return `${acc}\n ${argString}`; + } // append arg values to existing arg values if (lineLength > 0) return `${acc} ${argString}`; diff --git a/x-pack/plugins/canvas/common/lib/__tests__/ast.to_expression.js b/packages/kbn-interpreter/common/lib/ast.to_expression.test.js similarity index 83% rename from x-pack/plugins/canvas/common/lib/__tests__/ast.to_expression.js rename to packages/kbn-interpreter/common/lib/ast.to_expression.test.js index 4b5985832e6abb6..455b12f583f30a7 100644 --- a/x-pack/plugins/canvas/common/lib/__tests__/ast.to_expression.js +++ b/packages/kbn-interpreter/common/lib/ast.to_expression.test.js @@ -1,18 +1,39 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 expect from 'expect.js'; -import { toExpression } from '../ast'; +import { toExpression } from './ast'; describe('ast toExpression', () => { describe('single expression', () => { + it('throws if no type included', () => { + const errMsg = 'Objects must have a type property'; + const astObject = { hello: 'world' }; + expect(() => toExpression(astObject)).toThrowError(errMsg); + }); + it('throws if not correct type', () => { const errMsg = 'Expression must be an expression or argument function'; - const astObject = { hello: 'world' }; - expect(() => toExpression(astObject)).to.throwException(errMsg); + const astObject = { + type: 'hi', + hello: 'world', + }; + expect(() => toExpression(astObject)).toThrowError(errMsg); }); it('throws if expression without chain', () => { @@ -21,7 +42,7 @@ describe('ast toExpression', () => { type: 'expression', hello: 'world', }; - expect(() => toExpression(astObject)).to.throwException(errMsg); + expect(() => toExpression(astObject)).toThrowError(errMsg); }); it('throws if arguments type is invalid', () => { @@ -29,7 +50,7 @@ describe('ast toExpression', () => { const invalidTypes = [null, []]; function validate(obj) { - expect(() => toExpression(obj)).to.throwException(errMsg); + expect(() => toExpression(obj)).toThrowError(errMsg); } for (let i = 0; i < invalidTypes.length; i++) { @@ -56,12 +77,12 @@ describe('ast toExpression', () => { function: 'pointseries', arguments: null, }; - expect(() => toExpression(astObject)).to.throwException(errMsg); + expect(() => toExpression(astObject)).toThrowError(errMsg); }); it('throws on invalid argument type', () => { const argType = '__invalid__wat__'; - const errMsg = `invalid argument type: ${argType}`; + const errMsg = `Invalid argument type in AST: ${argType}`; const astObject = { type: 'expression', chain: [ @@ -80,7 +101,7 @@ describe('ast toExpression', () => { ], }; - expect(() => toExpression(astObject)).to.throwException(errMsg); + expect(() => toExpression(astObject)).toThrowError(errMsg); }); it('throws on expressions without chains', () => { @@ -104,7 +125,7 @@ describe('ast toExpression', () => { ], }; - expect(() => toExpression(astObject)).to.throwException(errMsg); + expect(() => toExpression(astObject)).toThrowError(errMsg); }); it('throws on nameless functions and partials', () => { @@ -120,7 +141,7 @@ describe('ast toExpression', () => { ], }; - expect(() => toExpression(astObject)).to.throwException(errMsg); + expect(() => toExpression(astObject)).toThrowError(errMsg); }); it('single expression', () => { @@ -136,7 +157,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('csv'); + expect(expression).toBe('csv'); }); it('single expression with string argument', () => { @@ -154,7 +175,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('csv input="stuff\nthings"'); + expect(expression).toBe('csv input="stuff\nthings"'); }); it('single expression string value with a backslash', () => { @@ -172,7 +193,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('csv input="slash \\\\\\\\ slash"'); + expect(expression).toBe('csv input="slash \\\\\\\\ slash"'); }); it('single expression string value with a double quote', () => { @@ -190,7 +211,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('csv input="stuff\nthings\n\\"such\\""'); + expect(expression).toBe('csv input="stuff\nthings\n\\"such\\""'); }); it('single expression with number argument', () => { @@ -208,7 +229,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('series input=1234'); + expect(expression).toBe('series input=1234'); }); it('single expression with boolean argument', () => { @@ -226,7 +247,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('series input=true'); + expect(expression).toBe('series input=true'); }); it('single expression with null argument', () => { @@ -244,7 +265,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('series input=null'); + expect(expression).toBe('series input=null'); }); it('single expression with multiple arguments', () => { @@ -263,7 +284,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('csv input="stuff\nthings" separator="\\\\n"'); + expect(expression).toBe('csv input="stuff\nthings" separator="\\\\n"'); }); it('single expression with multiple and repeated arguments', () => { @@ -282,12 +303,12 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal( + expect(expression).toBe( 'csv input="stuff\nthings" input="more,things\nmore,stuff" separator="\\\\n"' ); }); - it('single expression with expression argument', () => { + it('single expression with `getcalc` expression argument', () => { const astObj = { type: 'expression', chain: [ @@ -314,10 +335,10 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('csv calc={getcalc} input="stuff\nthings"'); + expect(expression).toBe('csv calc={getcalc} input="stuff\nthings"'); }); - it('single expression with expression argument', () => { + it('single expression with `partcalc` expression argument', () => { const astObj = { type: 'expression', chain: [ @@ -344,7 +365,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('csv calc={partcalc} input="stuff\nthings"'); + expect(expression).toBe('csv calc={partcalc} input="stuff\nthings"'); }); it('single expression with expression arguments, with arguments', () => { @@ -390,7 +411,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal( + expect(expression).toBe( 'csv sep={partcalc type="comma"} input="stuff\nthings" break={setBreak type="newline"}' ); }); @@ -468,7 +489,7 @@ describe('ast toExpression', () => { '2016,honda,fit,15890,', '2016,honda,civic,18640"\n| line x={distinct f="year"} y={sum f="price"} colors={distinct f="model"}', ]; - expect(expression).to.equal(expected.join('\n')); + expect(expression).toBe(expected.join('\n')); }); it('three chained expressions', () => { @@ -563,7 +584,7 @@ describe('ast toExpression', () => { '2016,honda,civic,18640"\n| pointseries x={distinct f="year"} y={sum f="price"} ' + 'colors={distinct f="model"}\n| line pallette={getColorPallette name="elastic"}', ]; - expect(expression).to.equal(expected.join('\n')); + expect(expression).toBe(expected.join('\n')); }); }); @@ -583,7 +604,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('list "one" "two" "three"'); + expect(expression).toBe('list "one" "two" "three"'); }); it('named and unnamed', () => { @@ -603,7 +624,7 @@ describe('ast toExpression', () => { }; const expression = toExpression(astObj); - expect(expression).to.equal('both named="example" another="item" "one" "two" "three"'); + expect(expression).toBe('both named="example" another="item" "one" "two" "three"'); }); }); }); diff --git a/x-pack/plugins/canvas/common/lib/fn.js b/packages/kbn-interpreter/common/lib/fn.js similarity index 54% rename from x-pack/plugins/canvas/common/lib/fn.js rename to packages/kbn-interpreter/common/lib/fn.js index 70948c76579b37e..c6b2fcbe6779928 100644 --- a/x-pack/plugins/canvas/common/lib/fn.js +++ b/packages/kbn-interpreter/common/lib/fn.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 { mapValues, includes } from 'lodash'; diff --git a/packages/kbn-interpreter/common/lib/functions_registry.js b/packages/kbn-interpreter/common/lib/functions_registry.js new file mode 100644 index 000000000000000..1c71707d84722fb --- /dev/null +++ b/packages/kbn-interpreter/common/lib/functions_registry.js @@ -0,0 +1,29 @@ +/* + * 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 { Registry } from './registry'; +import { Fn } from './fn'; + +class FunctionsRegistry extends Registry { + wrapper(obj) { + return new Fn(obj); + } +} + +export const functionsRegistry = new FunctionsRegistry(); diff --git a/packages/kbn-interpreter/common/lib/get_by_alias.js b/packages/kbn-interpreter/common/lib/get_by_alias.js new file mode 100644 index 000000000000000..d7bb1bbf9e79dc7 --- /dev/null +++ b/packages/kbn-interpreter/common/lib/get_by_alias.js @@ -0,0 +1,33 @@ +/* + * 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. + */ + +/** + * This is used for looking up function/argument definitions. It looks through + * the given object/array for a case-insensitive match, which could be either the + * `name` itself, or something under the `aliases` property. + */ +export function getByAlias(specs, name) { + const lowerCaseName = name.toLowerCase(); + return Object.values(specs).find(({ name, aliases }) => { + if (name.toLowerCase() === lowerCaseName) return true; + return (aliases || []).some(alias => { + return alias.toLowerCase() === lowerCaseName; + }); + }); +} diff --git a/packages/kbn-interpreter/common/lib/get_by_alias.test.js b/packages/kbn-interpreter/common/lib/get_by_alias.test.js new file mode 100644 index 000000000000000..9cfc37fd8f30413 --- /dev/null +++ b/packages/kbn-interpreter/common/lib/get_by_alias.test.js @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getByAlias } from './get_by_alias'; + +describe('getByAlias', () => { + const fnsObject = { + foo: { name: 'foo', aliases: ['f'] }, + bar: { name: 'bar', aliases: ['b'] }, + }; + + const fnsArray = [{ name: 'foo', aliases: ['f'] }, { name: 'bar', aliases: ['b'] }]; + + it('returns the function by name', () => { + expect(getByAlias(fnsObject, 'foo')).toBe(fnsObject.foo); + expect(getByAlias(fnsObject, 'bar')).toBe(fnsObject.bar); + expect(getByAlias(fnsArray, 'foo')).toBe(fnsArray[0]); + expect(getByAlias(fnsArray, 'bar')).toBe(fnsArray[1]); + }); + + it('returns the function by alias', () => { + expect(getByAlias(fnsObject, 'f')).toBe(fnsObject.foo); + expect(getByAlias(fnsObject, 'b')).toBe(fnsObject.bar); + expect(getByAlias(fnsArray, 'f')).toBe(fnsArray[0]); + expect(getByAlias(fnsArray, 'b')).toBe(fnsArray[1]); + }); + + it('returns the function by case-insensitive name', () => { + expect(getByAlias(fnsObject, 'FOO')).toBe(fnsObject.foo); + expect(getByAlias(fnsObject, 'BAR')).toBe(fnsObject.bar); + expect(getByAlias(fnsArray, 'FOO')).toBe(fnsArray[0]); + expect(getByAlias(fnsArray, 'BAR')).toBe(fnsArray[1]); + }); + + it('returns the function by case-insensitive alias', () => { + expect(getByAlias(fnsObject, 'F')).toBe(fnsObject.foo); + expect(getByAlias(fnsObject, 'B')).toBe(fnsObject.bar); + expect(getByAlias(fnsArray, 'F')).toBe(fnsArray[0]); + expect(getByAlias(fnsArray, 'B')).toBe(fnsArray[1]); + }); + + it('handles empty strings', () => { + const emptyStringFnsObject = { '': { name: '' } }; + const emptyStringAliasFnsObject = { foo: { name: 'foo', aliases: [''] } }; + expect(getByAlias(emptyStringFnsObject, '')).toBe(emptyStringFnsObject['']); + expect(getByAlias(emptyStringAliasFnsObject, '')).toBe(emptyStringAliasFnsObject.foo); + + const emptyStringFnsArray = [{ name: '' }]; + const emptyStringAliasFnsArray = [{ name: 'foo', aliases: [''] }]; + expect(getByAlias(emptyStringFnsArray, '')).toBe(emptyStringFnsArray[0]); + expect(getByAlias(emptyStringAliasFnsArray, '')).toBe(emptyStringAliasFnsArray[0]); + }); + + it('handles "undefined" strings', () => { + const undefinedFnsObject = { undefined: { name: 'undefined' } }; + const undefinedAliasFnsObject = { foo: { name: 'undefined', aliases: ['undefined'] } }; + expect(getByAlias(undefinedFnsObject, 'undefined')).toBe(undefinedFnsObject.undefined); + expect(getByAlias(undefinedAliasFnsObject, 'undefined')).toBe(undefinedAliasFnsObject.foo); + + const emptyStringFnsArray = [{ name: 'undefined' }]; + const emptyStringAliasFnsArray = [{ name: 'foo', aliases: ['undefined'] }]; + expect(getByAlias(emptyStringFnsArray, 'undefined')).toBe(emptyStringFnsArray[0]); + expect(getByAlias(emptyStringAliasFnsArray, 'undefined')).toBe(emptyStringAliasFnsArray[0]); + }); + + it('returns undefined if not found', () => { + expect(getByAlias(fnsObject, 'baz')).toBe(undefined); + expect(getByAlias(fnsArray, 'baz')).toBe(undefined); + }); +}); diff --git a/packages/kbn-interpreter/common/lib/get_type.js b/packages/kbn-interpreter/common/lib/get_type.js new file mode 100644 index 000000000000000..ac440acf8da5dc4 --- /dev/null +++ b/packages/kbn-interpreter/common/lib/get_type.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function getType(node) { + if (node == null) return 'null'; + if (typeof node === 'object') { + if (!node.type) throw new Error('Objects must have a type property'); + return node.type; + } + + return typeof node; +} diff --git a/x-pack/plugins/canvas/common/lib/grammar.js b/packages/kbn-interpreter/common/lib/grammar.js similarity index 100% rename from x-pack/plugins/canvas/common/lib/grammar.js rename to packages/kbn-interpreter/common/lib/grammar.js diff --git a/x-pack/plugins/canvas/common/lib/grammar.peg b/packages/kbn-interpreter/common/lib/grammar.peg similarity index 100% rename from x-pack/plugins/canvas/common/lib/grammar.peg rename to packages/kbn-interpreter/common/lib/grammar.peg diff --git a/packages/kbn-interpreter/common/lib/paths_registry.js b/packages/kbn-interpreter/common/lib/paths_registry.js new file mode 100644 index 000000000000000..3ad2b5dddf82e19 --- /dev/null +++ b/packages/kbn-interpreter/common/lib/paths_registry.js @@ -0,0 +1,65 @@ +/* + * 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. + */ + +class PathsRegistry { + + constructor() { + this.paths = new Map(); + } + + register = (type, paths) => { + if (!type) { + throw new Error(`Register requires a type`); + } + const lowerCaseType = type.toLowerCase(); + + const pathArray = Array.isArray(paths) ? paths : [paths]; + if (!this.paths.has(lowerCaseType)) { + this.paths.set(lowerCaseType, []); + } + + pathArray.forEach(p => { + this.paths.get(lowerCaseType).push(p); + }); + }; + + registerAll = (paths) => { + Object.keys(paths).forEach(type => { + this.register(type, paths[type]); + }); + }; + + toArray = () => { + return [...this.paths.values()]; + }; + + get = (type) => { + if (!type) { + return []; + } + const lowerCaseType = type.toLowerCase(); + return this.paths.has(lowerCaseType) ? this.paths.get(lowerCaseType) : []; + }; + + reset = () => { + this.paths.clear(); + }; +} + +export const pathsRegistry = new PathsRegistry(); diff --git a/packages/kbn-interpreter/common/lib/paths_registry.test.js b/packages/kbn-interpreter/common/lib/paths_registry.test.js new file mode 100644 index 000000000000000..ad2b9d949deb3f4 --- /dev/null +++ b/packages/kbn-interpreter/common/lib/paths_registry.test.js @@ -0,0 +1,92 @@ +/* + * 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. + */ + +describe('pathsRegistry', () => { + let registry; + beforeEach(() => { + jest.resetModules(); + registry = require('./paths_registry').pathsRegistry; + }); + + const paths = { + foo: 'bar', + sometype: [ + 'Here', + 'be', + 'more', + 'paths!' + ], + anothertype: ['with just one lonely path'] + }; + + it('throws when no type is provided', () => { + const check = () => registry.register(null, paths.foo); + expect(check).toThrowError(/requires a type/); + }); + + it('accepts paths as a string', () => { + registry.register('foo', paths.foo); + expect(registry.get('foo')).toEqual([paths.foo]); + }); + + it('accepts paths as an array', () => { + registry.register('sometype', paths.sometype); + expect(registry.get('sometype')).toEqual(paths.sometype); + }); + + it('ignores case when setting items', () => { + registry.register('FOO', paths.foo); + expect(registry.get('foo')).toEqual([paths.foo]); + }); + + it('gets items by lookup property', () => { + registry.register('sometype', paths.sometype); + expect(registry.get('sometype')).toEqual(paths.sometype); + }); + + it('can register an object of `type: path` key-value pairs', () => { + registry.registerAll(paths); + expect(registry.get('foo')).toEqual([paths.foo]); + expect(registry.get('sometype')).toEqual(paths.sometype); + expect(registry.get('anothertype')).toEqual(paths.anothertype); + }); + + it('ignores case when getting items', () => { + registry.registerAll(paths); + expect(registry.get('FOO')).toEqual([paths.foo]); + expect(registry.get('SOmEType')).toEqual(paths.sometype); + expect(registry.get('anoThertYPE')).toEqual(paths.anothertype); + }); + + it('returns an empty array with no match', () => { + expect(registry.get('@@nope_nope')).toEqual([]); + }); + + it('returns an array of all path values', () => { + registry.registerAll(paths); + expect(registry.toArray()).toEqual([[paths.foo], paths.sometype, paths.anothertype]); + }); + + it('resets the registry', () => { + registry.registerAll(paths); + expect(registry.get('sometype')).toEqual(paths.sometype); + registry.reset(); + expect(registry.get('sometype')).toEqual([]); + }); +}); \ No newline at end of file diff --git a/x-pack/plugins/canvas/common/lib/registry.js b/packages/kbn-interpreter/common/lib/registry.js similarity index 56% rename from x-pack/plugins/canvas/common/lib/registry.js rename to packages/kbn-interpreter/common/lib/registry.js index accabae4bc5ebba..9882f3abde72322 100644 --- a/x-pack/plugins/canvas/common/lib/registry.js +++ b/packages/kbn-interpreter/common/lib/registry.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 clone from 'lodash.clone'; @@ -22,8 +35,9 @@ export class Registry { const obj = fn(); - if (typeof obj !== 'object' || !obj[this._prop]) + if (typeof obj !== 'object' || !obj[this._prop]) { throw new Error(`Registered functions must return an object with a ${this._prop} property`); + } this._indexed[obj[this._prop].toLowerCase()] = this.wrapper(obj); } diff --git a/x-pack/plugins/canvas/common/lib/__tests__/registry.js b/packages/kbn-interpreter/common/lib/registry.test.js similarity index 60% rename from x-pack/plugins/canvas/common/lib/__tests__/registry.js rename to packages/kbn-interpreter/common/lib/registry.test.js index fd19bf0300417e8..dbeeb16dc1ff0cd 100644 --- a/x-pack/plugins/canvas/common/lib/__tests__/registry.js +++ b/packages/kbn-interpreter/common/lib/registry.test.js @@ -1,51 +1,63 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 expect from 'expect.js'; -import { Registry } from '../registry'; +import { Registry } from './registry'; function validateRegistry(registry, elements) { it('gets items by lookup property', () => { - expect(registry.get('__test2')).to.eql(elements[1]()); + expect(registry.get('__test2')).toEqual(elements[1]()); }); it('ignores case when getting items', () => { - expect(registry.get('__TeSt2')).to.eql(elements[1]()); - expect(registry.get('__tESt2')).to.eql(elements[1]()); + expect(registry.get('__TeSt2')).toEqual(elements[1]()); + expect(registry.get('__tESt2')).toEqual(elements[1]()); }); it('gets a shallow clone', () => { - expect(registry.get('__test2')).to.not.equal(elements[1]()); + expect(registry.get('__test2')).not.toBe(elements[1]()); }); it('is null with no match', () => { - expect(registry.get('@@nope_nope')).to.be(null); + expect(registry.get('@@nope_nope')).toBe(null); }); it('returns shallow clone of the whole registry via toJS', () => { const regAsJs = registry.toJS(); - expect(regAsJs).to.eql({ + expect(regAsJs).toEqual({ __test1: elements[0](), __test2: elements[1](), }); - expect(regAsJs.__test1).to.eql(elements[0]()); - expect(regAsJs.__test1).to.not.equal(elements[0]()); + expect(regAsJs.__test1).toEqual(elements[0]()); + expect(regAsJs.__test1).not.toBe(elements[0]()); }); it('returns shallow clone array via toArray', () => { const regAsArray = registry.toArray(); - expect(regAsArray).to.be.an(Array); - expect(regAsArray[0]).to.eql(elements[0]()); - expect(regAsArray[0]).to.not.equal(elements[0]()); + expect(regAsArray).toBeInstanceOf(Array); + expect(regAsArray[0]).toEqual(elements[0]()); + expect(regAsArray[0]).not.toBe(elements[0]()); }); it('resets the registry', () => { - expect(registry.get('__test2')).to.eql(elements[1]()); + expect(registry.get('__test2')).toEqual(elements[1]()); registry.reset(); - expect(registry.get('__test2')).to.equal(null); + expect(registry.get('__test2')).toBe(null); }); } @@ -70,12 +82,12 @@ describe('Registry', () => { validateRegistry(registry, elements); it('has a prop of name', () => { - expect(registry.getProp()).to.equal('name'); + expect(registry.getProp()).toBe('name'); }); it('throws when object is missing the lookup prop', () => { const check = () => registry.register(() => ({ hello: 'world' })); - expect(check).to.throwException(/object with a name property/i); + expect(check).toThrowError(/object with a name property/); }); }); @@ -99,12 +111,12 @@ describe('Registry', () => { validateRegistry(registry, elements); it('has a prop of type', () => { - expect(registry.getProp()).to.equal('type'); + expect(registry.getProp()).toBe('type'); }); it('throws when object is missing the lookup prop', () => { const check = () => registry.register(() => ({ hello: 'world' })); - expect(check).to.throwException(/object with a type property/i); + expect(check).toThrowError(/object with a type property/); }); }); @@ -137,9 +149,8 @@ describe('Registry', () => { registry.register(elements[1]); it('contains wrapped elements', () => { - // test for the custom prop on the returned elements - expect(registry.get(elements[0]().name)).to.have.property('__CUSTOM_PROP__', 1); - expect(registry.get(elements[1]().name)).to.have.property('__CUSTOM_PROP__', 2); + expect(registry.get(elements[0]().name)).toHaveProperty('__CUSTOM_PROP__'); + expect(registry.get(elements[1]().name)).toHaveProperty('__CUSTOM_PROP__'); }); }); @@ -171,20 +182,18 @@ describe('Registry', () => { }); it('get contains the full prototype', () => { - expect(thing().baseFunc).to.be.a('function'); - expect(registry.get(name).baseFunc).to.be.a('function'); + expect(typeof thing().baseFunc).toBe('function'); + expect(typeof registry.get(name).baseFunc).toBe('function'); }); it('toJS contains the full prototype', () => { const val = registry.toJS(); - expect(val[name].baseFunc).to.be.a('function'); + expect(typeof val[name].baseFunc).toBe('function'); }); }); describe('throws when lookup prop is not a string', () => { const check = () => new Registry(2); - expect(check).to.throwException(e => { - expect(e.message).to.be('Registry property name must be a string'); - }); + expect(check).toThrowError(/must be a string/); }); }); diff --git a/packages/kbn-interpreter/common/lib/serialize.js b/packages/kbn-interpreter/common/lib/serialize.js new file mode 100644 index 000000000000000..2f881db3c77e0c3 --- /dev/null +++ b/packages/kbn-interpreter/common/lib/serialize.js @@ -0,0 +1,37 @@ +/* + * 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 { get, identity } from 'lodash'; +import { getType } from './get_type'; + +export function serializeProvider(types) { + return { + serialize: provider('serialize'), + deserialize: provider('deserialize'), + }; + + function provider(key) { + return context => { + const type = getType(context); + const typeDef = types[type]; + const fn = get(typeDef, key) || identity; + return fn(context); + }; + } +} diff --git a/x-pack/plugins/canvas/common/lib/type.js b/packages/kbn-interpreter/common/lib/type.js similarity index 61% rename from x-pack/plugins/canvas/common/lib/type.js rename to packages/kbn-interpreter/common/lib/type.js index d917750e3848e90..356b82bf91cbd0c 100644 --- a/x-pack/plugins/canvas/common/lib/type.js +++ b/packages/kbn-interpreter/common/lib/type.js @@ -1,12 +1,25 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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. */ // All types must be universal and be castable on the client or on the server import { get } from 'lodash'; -import { getType } from '../lib/get_type'; +import { getType } from './get_type'; // TODO: Currently all casting functions must be syncronous. @@ -35,10 +48,12 @@ export function Type(config) { this.to = (node, toTypeName, types) => { const typeName = getType(node); - if (typeName !== this.name) + if (typeName !== this.name) { throw new Error(`Can not cast object of type '${typeName}' using '${this.name}'`); - else if (!this.castsTo(toTypeName)) + } + else if (!this.castsTo(toTypeName)) { throw new Error(`Can not cast '${typeName}' to '${toTypeName}'`); + } return getToFn(toTypeName)(node, types); }; diff --git a/packages/kbn-interpreter/common/lib/types_registry.js b/packages/kbn-interpreter/common/lib/types_registry.js new file mode 100644 index 000000000000000..97e28875d7e201c --- /dev/null +++ b/packages/kbn-interpreter/common/lib/types_registry.js @@ -0,0 +1,29 @@ +/* + * 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 { Registry } from './registry'; +import { Type } from './type'; + +class TypesRegistry extends Registry { + wrapper(obj) { + return new Type(obj); + } +} + +export const typesRegistry = new TypesRegistry(); diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json new file mode 100644 index 000000000000000..0178b9e2cfe3276 --- /dev/null +++ b/packages/kbn-interpreter/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/interpreter", + "version": "1.0.0", + "license": "Apache-2.0", + "scripts": { + "build": "node tasks/build.js", + "canvas:peg": "pegjs common/lib/grammar.peg", + "kbn:bootstrap": "yarn build" + } +} diff --git a/packages/kbn-interpreter/plugin_src/functions/common/clog.js b/packages/kbn-interpreter/plugin_src/functions/common/clog.js new file mode 100644 index 000000000000000..634d166f5f0bb53 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/functions/common/clog.js @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const clog = () => ({ + name: 'clog', + help: 'Outputs the context to the console', + fn: context => { + console.log(context); //eslint-disable-line no-console + return context; + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/functions/common/index.js b/packages/kbn-interpreter/plugin_src/functions/common/index.js new file mode 100644 index 000000000000000..2f5f91181faec4b --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/functions/common/index.js @@ -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 { clog } from './clog'; + +export const commonFunctions = [ + clog, +]; diff --git a/packages/kbn-interpreter/plugin_src/functions/common/register.js b/packages/kbn-interpreter/plugin_src/functions/common/register.js new file mode 100644 index 000000000000000..8b146b8f849c370 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/functions/common/register.js @@ -0,0 +1,23 @@ +/* + * 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 { commonFunctions } from './index'; + +// eslint-disable-next-line no-undef +commonFunctions.forEach(canvas.register); diff --git a/packages/kbn-interpreter/plugin_src/types/boolean.js b/packages/kbn-interpreter/plugin_src/types/boolean.js new file mode 100644 index 000000000000000..cc5f0a79e39a84b --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/boolean.js @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const boolean = () => ({ + name: 'boolean', + from: { + null: () => false, + number: n => Boolean(n), + string: s => Boolean(s), + }, + to: { + render: value => { + const text = `${value}`; + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + datatable: value => ({ + type: 'datatable', + columns: [{ name: 'value', type: 'boolean' }], + rows: [{ value }], + }), + }, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/datatable.js b/packages/kbn-interpreter/plugin_src/types/datatable.js similarity index 63% rename from x-pack/plugins/canvas/canvas_plugin_src/types/datatable.js rename to packages/kbn-interpreter/plugin_src/types/datatable.js index cfe75605f1ebf2d..92bd2c9b1b59ec3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/datatable.js +++ b/packages/kbn-interpreter/plugin_src/types/datatable.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 { map, zipObject } from 'lodash'; @@ -10,8 +23,9 @@ export const datatable = () => ({ name: 'datatable', validate: datatable => { // TODO: Check columns types. Only string, boolean, number, date, allowed for now. - if (!datatable.columns) + if (!datatable.columns) { throw new Error('datatable must have a columns array, even if it is empty'); + } if (!datatable.rows) throw new Error('datatable must have a rows array, even if it is empty'); }, diff --git a/packages/kbn-interpreter/plugin_src/types/error.js b/packages/kbn-interpreter/plugin_src/types/error.js new file mode 100644 index 000000000000000..1415a065d810e82 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/error.js @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const error = () => ({ + name: 'error', + to: { + render: input => { + const { error, info } = input; + return { + type: 'render', + as: 'error', + value: { + error, + info, + }, + }; + }, + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/filter.js b/packages/kbn-interpreter/plugin_src/types/filter.js new file mode 100644 index 000000000000000..484050671b2f95e --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/filter.js @@ -0,0 +1,33 @@ +/* + * 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 const filter = () => ({ + name: 'filter', + from: { + null: () => { + return { + type: 'filter', + // Any meta data you wish to pass along. + meta: {}, + // And filters. If you need an "or", create a filter type for it. + and: [], + }; + }, + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/image.js b/packages/kbn-interpreter/plugin_src/types/image.js new file mode 100644 index 000000000000000..7666451145f5d39 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/image.js @@ -0,0 +1,31 @@ +/* + * 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 const image = () => ({ + name: 'image', + to: { + render: input => { + return { + type: 'render', + as: 'image', + value: input, + }; + }, + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/index.js b/packages/kbn-interpreter/plugin_src/types/index.js new file mode 100644 index 000000000000000..1ae5f874835c309 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/index.js @@ -0,0 +1,46 @@ +/* + * 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 { boolean } from './boolean'; +import { datatable } from './datatable'; +import { error } from './error'; +import { filter } from './filter'; +import { image } from './image'; +import { nullType } from './null'; +import { number } from './number'; +import { pointseries } from './pointseries'; +import { render } from './render'; +import { shape } from './shape'; +import { string } from './string'; +import { style } from './style'; + +export const typeSpecs = [ + boolean, + datatable, + error, + filter, + image, + number, + nullType, + pointseries, + render, + shape, + string, + style, +]; diff --git a/packages/kbn-interpreter/plugin_src/types/null.js b/packages/kbn-interpreter/plugin_src/types/null.js new file mode 100644 index 000000000000000..2789ce330ac6c4e --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/null.js @@ -0,0 +1,25 @@ +/* + * 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 const nullType = () => ({ + name: 'null', + from: { + '*': () => null, + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/number.js b/packages/kbn-interpreter/plugin_src/types/number.js new file mode 100644 index 000000000000000..8f8f31ea8a2fb17 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/number.js @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const number = () => ({ + name: 'number', + from: { + null: () => 0, + boolean: b => Number(b), + string: n => Number(n), + }, + to: { + render: value => { + const text = `${value}`; + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + datatable: value => ({ + type: 'datatable', + columns: [{ name: 'value', type: 'number' }], + rows: [{ value }], + }), + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/pointseries.js b/packages/kbn-interpreter/plugin_src/types/pointseries.js new file mode 100644 index 000000000000000..2275ea9e0409443 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/pointseries.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const pointseries = () => ({ + name: 'pointseries', + from: { + null: () => { + return { + type: 'pointseries', + rows: [], + columns: [], + }; + }, + }, + to: { + render: (pointseries, types) => { + const datatable = types.datatable.from(pointseries, types); + return { + type: 'render', + as: 'table', + value: { + datatable, + showHeader: true, + }, + }; + }, + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/register.js b/packages/kbn-interpreter/plugin_src/types/register.js new file mode 100644 index 000000000000000..17b03f0229672df --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/register.js @@ -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 'babel-polyfill'; +import { typeSpecs } from './index'; + +// eslint-disable-next-line no-undef +typeSpecs.forEach(canvas.register); diff --git a/packages/kbn-interpreter/plugin_src/types/render.js b/packages/kbn-interpreter/plugin_src/types/render.js new file mode 100644 index 000000000000000..99ce3ca7d1cd7db --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/render.js @@ -0,0 +1,29 @@ +/* + * 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 const render = () => ({ + name: 'render', + from: { + '*': v => ({ + type: 'render', + as: 'debug', + value: v, + }), + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/shape.js b/packages/kbn-interpreter/plugin_src/types/shape.js new file mode 100644 index 000000000000000..1ed7a111268d1f7 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/shape.js @@ -0,0 +1,31 @@ +/* + * 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 const shape = () => ({ + name: 'shape', + to: { + render: input => { + return { + type: 'render', + as: 'shape', + value: input, + }; + }, + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/string.js b/packages/kbn-interpreter/plugin_src/types/string.js new file mode 100644 index 000000000000000..90e6b17cc9dcf00 --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/string.js @@ -0,0 +1,41 @@ +/* + * 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 const string = () => ({ + name: 'string', + from: { + null: () => '', + boolean: b => String(b), + number: n => String(n), + }, + to: { + render: text => { + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + datatable: value => ({ + type: 'datatable', + columns: [{ name: 'value', type: 'string' }], + rows: [{ value }], + }), + }, +}); diff --git a/packages/kbn-interpreter/plugin_src/types/style.js b/packages/kbn-interpreter/plugin_src/types/style.js new file mode 100644 index 000000000000000..97057b415a4754f --- /dev/null +++ b/packages/kbn-interpreter/plugin_src/types/style.js @@ -0,0 +1,31 @@ +/* + * 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 const style = () => ({ + name: 'style', + from: { + null: () => { + return { + type: 'style', + spec: {}, + css: '', + }; + }, + }, +}); diff --git a/packages/kbn-interpreter/public/browser_registries.js b/packages/kbn-interpreter/public/browser_registries.js new file mode 100644 index 000000000000000..778a0b03a762421 --- /dev/null +++ b/packages/kbn-interpreter/public/browser_registries.js @@ -0,0 +1,83 @@ +/* + * 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 chrome from 'ui/chrome'; +import $script from 'scriptjs'; + +let resolvePromise = null; +let called = false; + +let populatePromise = new Promise(_resolve => { + resolvePromise = _resolve; +}); + +export const getBrowserRegistries = () => { + return populatePromise; +}; + +const loadBrowserRegistries = (registries) => { + const remainingTypes = Object.keys(registries); + const populatedTypes = {}; + + return new Promise(resolve => { + function loadType() { + if (!remainingTypes.length) { + resolve(populatedTypes); + return; + } + const type = remainingTypes.pop(); + window.canvas = window.canvas || {}; + window.canvas.register = d => registries[type].register(d); + + // Load plugins one at a time because each needs a different loader function + // $script will only load each of these once, we so can call this as many times as we need? + const pluginPath = chrome.addBasePath(`/api/canvas/plugins?type=${type}`); + $script(pluginPath, () => { + populatedTypes[type] = registries[type]; + loadType(); + }); + } + + loadType(); + }); +}; + +export const populateBrowserRegistries = (registries) => { + if (called) { + const oldPromise = populatePromise; + let newResolve; + populatePromise = new Promise(_resolve => { + newResolve = _resolve; + }); + oldPromise.then(oldTypes => { + loadBrowserRegistries(registries).then(newTypes => { + newResolve({ + ...oldTypes, + ...newTypes, + }); + }); + }); + return populatePromise; + } + called = true; + loadBrowserRegistries(registries).then(registries => { + resolvePromise(registries); + }); + return populatePromise; +}; diff --git a/packages/kbn-interpreter/public/create_handlers.js b/packages/kbn-interpreter/public/create_handlers.js new file mode 100644 index 000000000000000..3446a945ae76e39 --- /dev/null +++ b/packages/kbn-interpreter/public/create_handlers.js @@ -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. + */ + +export function createHandlers(/*socket*/) { + return { + environment: 'client', + }; +} diff --git a/x-pack/plugins/canvas/public/lib/interpreter.js b/packages/kbn-interpreter/public/interpreter.js similarity index 55% rename from x-pack/plugins/canvas/public/lib/interpreter.js rename to packages/kbn-interpreter/public/interpreter.js index 36878871b8b150c..5c1e199bce363f9 100644 --- a/x-pack/plugins/canvas/public/lib/interpreter.js +++ b/packages/kbn-interpreter/public/interpreter.js @@ -1,15 +1,28 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { socketInterpreterProvider } from '../../common/interpreter/socket_interpret'; -import { serializeProvider } from '../../common/lib/serialize'; -import { getSocket } from '../socket'; -import { typesRegistry } from '../../common/lib/types_registry'; +import { socketInterpreterProvider } from '../common/interpreter/socket_interpret'; +import { serializeProvider } from '../common/lib/serialize'; +import { getSocket } from './socket'; +import { typesRegistry } from '../common/lib/types_registry'; import { createHandlers } from './create_handlers'; -import { functionsRegistry } from './functions_registry'; +import { functionsRegistry } from '../common/lib/functions_registry'; import { getBrowserRegistries } from './browser_registries'; let socket; diff --git a/x-pack/plugins/canvas/public/socket.js b/packages/kbn-interpreter/public/socket.js similarity index 64% rename from x-pack/plugins/canvas/public/socket.js rename to packages/kbn-interpreter/public/socket.js index 92deedd488c0623..9143f0018377b6b 100644 --- a/x-pack/plugins/canvas/public/socket.js +++ b/packages/kbn-interpreter/public/socket.js @@ -1,12 +1,25 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 io from 'socket.io-client'; import { functionsRegistry } from '../common/lib/functions_registry'; -import { getBrowserRegistries } from './lib/browser_registries'; +import { getBrowserRegistries } from './browser_registries'; const SOCKET_CONNECTION_TIMEOUT = 5000; // timeout in ms let socket; diff --git a/packages/kbn-interpreter/server/get_plugin_paths.js b/packages/kbn-interpreter/server/get_plugin_paths.js new file mode 100644 index 000000000000000..f6520563c912ffb --- /dev/null +++ b/packages/kbn-interpreter/server/get_plugin_paths.js @@ -0,0 +1,55 @@ +/* + * 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 fs from 'fs'; +import { resolve } from 'path'; +import { promisify } from 'util'; +import { flatten } from 'lodash'; +import { pathsRegistry } from '../common/lib/paths_registry'; + +const lstat = promisify(fs.lstat); +const readdir = promisify(fs.readdir); + +const isDirectory = path => + lstat(path) + .then(stat => stat.isDirectory()) + .catch(() => false); + +export const getPluginPaths = type => { + const typePaths = pathsRegistry.get(type); + if (!typePaths) { + throw new Error(`Unknown type: ${type}`); + } + + return Promise.all(typePaths.map(async path => { + const isDir = await isDirectory(path); + if (!isDir) { + return; + } + // Get the full path of all js files in the directory + return readdir(path).then(files => { + return files.reduce((acc, file) => { + if (file.endsWith('.js')) { + acc.push(resolve(path, file)); + } + return acc; + }, []); + }).catch(); + })).then(flatten); +}; diff --git a/packages/kbn-interpreter/server/server_registries.js b/packages/kbn-interpreter/server/server_registries.js new file mode 100644 index 000000000000000..3fbb957673e639a --- /dev/null +++ b/packages/kbn-interpreter/server/server_registries.js @@ -0,0 +1,78 @@ +/* + * 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 { typesRegistry } from '../common/lib/types_registry'; +import { functionsRegistry as serverFunctions } from '../common/lib/functions_registry'; +import { getPluginPaths } from './get_plugin_paths'; + +const registries = { + serverFunctions: serverFunctions, + commonFunctions: serverFunctions, + types: typesRegistry, +}; + +let resolve = null; +let called = false; + +const populatePromise = new Promise(_resolve => { + resolve = _resolve; +}); + +export const getServerRegistries = () => { + return populatePromise; +}; + +export const populateServerRegistries = types => { + if (called) { + console.log('function should only be called once per process'); + return populatePromise; + } + called = true; + if (!types || !types.length) throw new Error('types is required'); + + const remainingTypes = types; + const populatedTypes = {}; + + const globalKeys = Object.keys(global); + + const loadType = () => { + const type = remainingTypes.pop(); + getPluginPaths(type).then(paths => { + global.canvas = global.canvas || {}; + global.canvas.register = d => registries[type].register(d); + + paths.forEach(path => { + require(path); + }); + + Object.keys(global).forEach(key => { + if (!globalKeys.includes(key)) { + delete global[key]; + } + }); + + populatedTypes[type] = registries[type]; + if (remainingTypes.length) loadType(); + else resolve(populatedTypes); + }); + }; + + if (remainingTypes.length) loadType(); + return populatePromise; +}; diff --git a/packages/kbn-interpreter/tasks/build.js b/packages/kbn-interpreter/tasks/build.js new file mode 100644 index 000000000000000..37776e8d74cca86 --- /dev/null +++ b/packages/kbn-interpreter/tasks/build.js @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const webpack = require('webpack'); +const webpackConfig = require('./webpack.plugins'); + +const devtool = 'inline-cheap-module-source-map'; + +const onComplete = function (done) { + return function (err, stats) { + if (err) { + done && done(err); + } else { + const seconds = ((stats.endTime - stats.startTime) / 1000).toFixed(2); + console.log(`Plugins built in ${seconds} seconds`); + done && done(); + } + }; +}; + +webpack({ ...webpackConfig, devtool }, onComplete(function () { + console.log('all done'); +})); + diff --git a/packages/kbn-interpreter/tasks/webpack.plugins.js b/packages/kbn-interpreter/tasks/webpack.plugins.js new file mode 100644 index 000000000000000..8b16edc5ad46222 --- /dev/null +++ b/packages/kbn-interpreter/tasks/webpack.plugins.js @@ -0,0 +1,100 @@ +/* + * 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. + */ + +const path = require('path'); + +const sourceDir = path.resolve(__dirname, '../plugin_src'); +const buildDir = path.resolve(__dirname, '../plugin'); + +module.exports = { + entry: { + 'types/all': path.join(sourceDir, 'types/register.js'), + 'functions/common/all': path.join(sourceDir, 'functions/common/register.js'), + }, + target: 'webworker', + + output: { + path: buildDir, + filename: '[name].js', // Need long paths here. + libraryTarget: 'umd', + }, + + resolve: { + extensions: ['.js', '.json'], + mainFields: ['browser', 'main'], + }, + + plugins: [ + function loaderFailHandler() { + // bails on error, including loader errors + // see https://github.com/webpack/webpack/issues/708, which does not fix loader errors + let isWatch = true; + + this.plugin('run', function (compiler, callback) { + isWatch = false; + callback.call(compiler); + }); + + this.plugin('done', function (stats) { + if (!stats.hasErrors()) { + return; + } + const errorMessage = stats.toString('errors-only'); + if (isWatch) { + console.error(errorMessage); + } + else { + throw new Error(errorMessage); + } + }); + }, + ], + + module: { + rules: [ + { + test: /\.js$/, + exclude: [/node_modules/], + loaders: 'babel-loader', + options: { + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, + }, + { + test: /\.(png|jpg|gif|jpeg|svg)$/, + loaders: ['url-loader'], + }, + { + test: /\.(css|scss)$/, + loaders: ['style-loader', 'css-loader', 'sass-loader'], + }, + ], + }, + + node: { + // Don't replace built-in globals + __filename: false, + __dirname: false, + }, + + watchOptions: { + ignored: [/node_modules/], + }, +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/package.json b/packages/kbn-plugin-generator/sao_template/template/package.json index 073571613ba4a1e..8dd24a57c928d4b 100755 --- a/packages/kbn-plugin-generator/sao_template/template/package.json +++ b/packages/kbn-plugin-generator/sao_template/template/package.json @@ -26,6 +26,7 @@ "eslint-plugin-babel": "^5.2.0", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jest": "^21.22.1", + "eslint-plugin-jsx-a11y": "^6.1.2", "eslint-plugin-mocha": "^5.2.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", diff --git a/packages/kbn-pm/README.md b/packages/kbn-pm/README.md index c698b6985b857fa..0d077fa380482c9 100644 --- a/packages/kbn-pm/README.md +++ b/packages/kbn-pm/README.md @@ -52,7 +52,24 @@ scripts, while still having a nice developer experience. ## How it works -The approach we went for to handle multiple packages in Kibana is relying on +### Internal usage + +For packages that are referenced within the Kibana repo itself (for example, +using the `@kbn/datemath` package from an `x-pack` plugin), we are leveraging +Yarn's workspaces feature. This allows yarn to optimize node_modules within +the entire repo to avoid duplicate modules by hoisting common packages as high +in the dependency tree as possible. + +To reference a package from within the Kibana repo, simply use the current +version number from that package's package.json file. Then, running `yarn kbn +bootstrap` will symlink that package into your dependency tree. That means +you can make changes to `@kbn/datematch` and immediately have them available +in Kibana itself. No `npm publish` needed anymore — Kibana will always rely +directly on the code that's in the local packages. + +### External Plugins + +For external plugins, referencing packages in Kibana relies on `link:` style dependencies in Yarn. With `link:` dependencies you specify the relative location to a package instead of a version when adding it to `package.json`. For example: @@ -62,11 +79,9 @@ relative location to a package instead of a version when adding it to ``` Now when you run `yarn` it will set up a symlink to this folder instead of -downloading code from the npm registry. That means you can make changes to -`@kbn/datematch` and immediately have them available in Kibana itself. No -`npm publish` needed anymore — Kibana will always rely directly on the code -that's in the local packages. And we can also do the same in x-pack-kibana or -any other Kibana plugin, e.g. +downloading code from the npm registry. This allows external plugins to always +use the versions of the package that is bundled with the Kibana version they +are running inside of. ``` "@kbn/datemath": "link:../../kibana/packages/kbn-date-math" @@ -191,10 +206,10 @@ While exploring the approach to static dependencies we built PoCs using npm 5 workspaces][yarn-workspaces], Yarn (using `link:` dependencies), and [Lerna][lerna]. -In the end we decided to build our own tool, based on Yarn and `link:` -dependencies. This gave us the control we wanted, and it fits nicely into our -context (e.g. where publishing to npm isn't necessarily something we want to -do). +In the end we decided to build our own tool, based on Yarn, and `link:` +dependencies, and workspaces. This gave us the control we wanted, and it fits +nicely into our context (e.g. where publishing to npm isn't necessarily +something we want to do). ### Some notes from this exploration diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 93c0e111f0d38e7..56a18caef6407e3 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -11064,7 +11064,8 @@ let run = exports.run = (() => { e: 'exclude', h: 'help', i: 'include' - } + }, + boolean: ['prefer-offline', 'frozen-lockfile'] }); const args = options._; if (options.help || args.length === 0) { @@ -12199,8 +12200,7 @@ const BootstrapCommand = exports.BootstrapCommand = { batchByWorkspace: true }); const batchedProjects = (0, _projects.topologicallyBatchProjects)(projects, projectGraph); - const frozenLockfile = options['frozen-lockfile'] === true; - const extraArgs = frozenLockfile ? ['--frozen-lockfile'] : []; + const extraArgs = [...(options['frozen-lockfile'] === true ? ['--frozen-lockfile'] : []), ...(options['prefer-offline'] === true ? ['--prefer-offline'] : [])]; _log.log.write(_chalk2.default.bold('\nRunning installs in topological order:')); for (const batch of batchedProjectsByWorkspace) { for (const project of batch) { diff --git a/packages/kbn-pm/src/cli.ts b/packages/kbn-pm/src/cli.ts index 07383f89b816505..d7e3aafd13fceb7 100644 --- a/packages/kbn-pm/src/cli.ts +++ b/packages/kbn-pm/src/cli.ts @@ -65,6 +65,7 @@ export async function run(argv: string[]) { h: 'help', i: 'include', }, + boolean: ['prefer-offline', 'frozen-lockfile'], }); const args = options._; diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 2469e4cca1cb185..be4b9da7bf51629 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -35,8 +35,10 @@ export const BootstrapCommand: ICommand = { }); const batchedProjects = topologicallyBatchProjects(projects, projectGraph); - const frozenLockfile = options['frozen-lockfile'] === true; - const extraArgs = frozenLockfile ? ['--frozen-lockfile'] : []; + const extraArgs = [ + ...(options['frozen-lockfile'] === true ? ['--frozen-lockfile'] : []), + ...(options['prefer-offline'] === true ? ['--prefer-offline'] : []), + ]; log.write(chalk.bold('\nRunning installs in topological order:')); diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap index 93d129213ff025d..c55b8abf6c78c4a 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap @@ -18,6 +18,7 @@ Options: --updateBaselines Replace baseline screenshots with whatever is generated from the test. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. + --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. --verbose Log everything. --debug Run in debug mode. --quiet Only log errors. @@ -26,8 +27,9 @@ Options: exports[`process options for run tests CLI accepts boolean value for updateBaselines 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": undefined, @@ -41,8 +43,9 @@ Object { exports[`process options for run tests CLI accepts debug option 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "debug": true, @@ -56,9 +59,10 @@ Object { exports[`process options for run tests CLI accepts empty config value if default passed 1`] = ` Object { + "assertNoneExcluded": false, "config": "", "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": undefined, @@ -74,8 +78,9 @@ Object { "_": Object { "server.foo": "bar", }, + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": Object { @@ -90,8 +95,9 @@ Object { exports[`process options for run tests CLI accepts quiet option 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": undefined, @@ -105,8 +111,9 @@ Object { exports[`process options for run tests CLI accepts silent option 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": undefined, @@ -120,8 +127,9 @@ Object { exports[`process options for run tests CLI accepts source value for esFrom 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "esFrom": "source", @@ -135,8 +143,9 @@ Object { exports[`process options for run tests CLI accepts string value for kibana-install-dir 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": undefined, @@ -150,8 +159,9 @@ Object { exports[`process options for run tests CLI accepts value for grep 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": undefined, @@ -165,8 +175,9 @@ Object { exports[`process options for run tests CLI accepts verbose option 1`] = ` Object { + "assertNoneExcluded": false, "configs": Array [ - "foo", + /foo, ], "createLogger": [Function], "extraKbnOpts": undefined, diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap index 6d4b35f754b425e..e9f3dfac56746dd 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap @@ -18,6 +18,7 @@ Options: --updateBaselines Replace baseline screenshots with whatever is generated from the test. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. + --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. --verbose Log everything. --debug Run in debug mode. --quiet Only log errors. diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index 6aa1e1aa5c31b5a..eb7809b91cebc32 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -17,6 +17,8 @@ * under the License. */ +import { resolve } from 'path'; + import dedent from 'dedent'; import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; @@ -52,6 +54,9 @@ const options = { arg: '', desc: 'Tags that suites must NOT include to be run, can be included multiple times.', }, + 'assert-none-excluded': { + desc: 'Exit with 1/0 based on if any test is excluded with the current set of tags.', + }, verbose: { desc: 'Log everything.' }, debug: { desc: 'Run in debug mode.' }, quiet: { desc: 'Only log errors.' }, @@ -113,6 +118,9 @@ export function processOptions(userOptions, defaultConfigPaths) { delete userOptions['include-tag']; delete userOptions['exclude-tag']; + userOptions.assertNoneExcluded = !!userOptions['assert-none-excluded']; + delete userOptions['assert-none-excluded']; + function createLogger() { return new ToolingLog({ level: pickLevelFromFlags(userOptions), @@ -122,7 +130,7 @@ export function processOptions(userOptions, defaultConfigPaths) { return { ...userOptions, - configs, + configs: configs.map(c => resolve(c)), createLogger, extraKbnOpts: userOptions._, }; diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js index 2e0c2fb44022dc2..a6d5bf72ffc8549 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js @@ -18,6 +18,9 @@ */ import { displayHelp, processOptions } from './args'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer(process.cwd())); describe('display help for run tests CLI', () => { it('displays as expected', () => { diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js index 7e2e6c5a40e7830..90caaf2cc6ce4f7 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js @@ -17,6 +17,8 @@ * under the License. */ +import { Writable } from 'stream'; + import { runTestsCli } from './cli'; import { checkMockConsoleLogSnapshot } from '../../test_helpers'; @@ -36,7 +38,7 @@ describe('run tests CLI', () => { const processMock = { exit: exitMock, argv: argvMock, - stdout: { on: jest.fn(), once: jest.fn(), emit: jest.fn() }, + stdout: new Writable(), cwd: jest.fn(), }; diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap index 50da9f683688356..106ddcb00faf694 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap @@ -21,9 +21,7 @@ Options: exports[`process options for start servers CLI accepts debug option 1`] = ` Object { - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "debug": true, "esFrom": "snapshot", @@ -33,9 +31,7 @@ Object { exports[`process options for start servers CLI accepts empty config value if default passed 1`] = ` Object { - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, @@ -47,9 +43,7 @@ Object { "_": Object { "server.foo": "bar", }, - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": Object { @@ -60,9 +54,7 @@ Object { exports[`process options for start servers CLI accepts quiet option 1`] = ` Object { - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, @@ -72,9 +64,7 @@ Object { exports[`process options for start servers CLI accepts silent option 1`] = ` Object { - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, @@ -84,9 +74,7 @@ Object { exports[`process options for start servers CLI accepts source value for esFrom 1`] = ` Object { - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, @@ -95,9 +83,7 @@ Object { exports[`process options for start servers CLI accepts string value for kibana-install-dir 1`] = ` Object { - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, @@ -107,9 +93,7 @@ Object { exports[`process options for start servers CLI accepts verbose option 1`] = ` Object { - "config": Array [ - "foo", - ], + "config": /foo, "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js index 170de1942232bb7..026c23ab46f37e5 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js @@ -17,6 +17,8 @@ * under the License. */ +import { resolve } from 'path'; + import dedent from 'dedent'; import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; @@ -97,7 +99,7 @@ export function processOptions(userOptions, defaultConfigPath) { return { ...userOptions, - config, + config: resolve(config), createLogger, extraKbnOpts: userOptions._, }; diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/args.test.js b/packages/kbn-test/src/functional_tests/cli/start_servers/args.test.js index 22c06b6ae323185..bc2b958d793fd28 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/args.test.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/args.test.js @@ -18,6 +18,9 @@ */ import { displayHelp, processOptions } from './args'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer(process.cwd())); describe('display help for start servers CLI', () => { it('displays as expected', () => { @@ -39,60 +42,60 @@ describe('process options for start servers CLI', () => { }); it('accepts empty config value if default passed', () => { - const options = processOptions({ config: '' }, ['foo']); + const options = processOptions({ config: '' }, 'foo'); expect(options).toMatchSnapshot(); }); it('rejects invalid option', () => { expect(() => { - processOptions({ bail: true }, ['foo']); + processOptions({ bail: true }, 'foo'); }).toThrow('functional_tests_server: invalid option [bail]'); }); it('accepts string value for kibana-install-dir', () => { - const options = processOptions({ 'kibana-install-dir': 'foo' }, ['foo']); + const options = processOptions({ 'kibana-install-dir': 'foo' }, 'foo'); expect(options).toMatchSnapshot(); }); it('rejects boolean value for kibana-install-dir', () => { expect(() => { - processOptions({ 'kibana-install-dir': true }, ['foo']); + processOptions({ 'kibana-install-dir': true }, 'foo'); }).toThrow('functional_tests_server: invalid argument [true] to option [kibana-install-dir]'); }); it('accepts source value for esFrom', () => { - const options = processOptions({ esFrom: 'source' }, ['foo']); + const options = processOptions({ esFrom: 'source' }, 'foo'); expect(options).toMatchSnapshot(); }); it('accepts debug option', () => { - const options = processOptions({ debug: true }, ['foo']); + const options = processOptions({ debug: true }, 'foo'); expect(options).toMatchSnapshot(); }); it('accepts silent option', () => { - const options = processOptions({ silent: true }, ['foo']); + const options = processOptions({ silent: true }, 'foo'); expect(options).toMatchSnapshot(); }); it('accepts quiet option', () => { - const options = processOptions({ quiet: true }, ['foo']); + const options = processOptions({ quiet: true }, 'foo'); expect(options).toMatchSnapshot(); }); it('accepts verbose option', () => { - const options = processOptions({ verbose: true }, ['foo']); + const options = processOptions({ verbose: true }, 'foo'); expect(options).toMatchSnapshot(); }); it('accepts extra server options', () => { - const options = processOptions({ _: { 'server.foo': 'bar' } }, ['foo']); + const options = processOptions({ _: { 'server.foo': 'bar' } }, 'foo'); expect(options).toMatchSnapshot(); }); it('rejects invalid options even if valid options exist', () => { expect(() => { - processOptions({ debug: true, aintnothang: true, bail: true }, ['foo']); + processOptions({ debug: true, aintnothang: true, bail: true }, 'foo'); }).toThrow('functional_tests_server: invalid option [aintnothang]'); }); }); diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js index 43bab5a4afe892c..5a195c813f39077 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js @@ -17,6 +17,8 @@ * under the License. */ +import { Writable } from 'stream'; + import { startServersCli } from './cli'; import { checkMockConsoleLogSnapshot } from '../../test_helpers'; @@ -36,7 +38,7 @@ describe('start servers CLI', () => { const processMock = { exit: exitMock, argv: argvMock, - stdout: { on: jest.fn(), once: jest.fn(), emit: jest.fn() }, + stdout: new Writable(), cwd: jest.fn(), }; diff --git a/packages/kbn-test/src/functional_tests/lib/index.js b/packages/kbn-test/src/functional_tests/lib/index.js index d66f641c10f68fa..ec381b56b06995f 100644 --- a/packages/kbn-test/src/functional_tests/lib/index.js +++ b/packages/kbn-test/src/functional_tests/lib/index.js @@ -19,6 +19,6 @@ export { runKibanaServer } from './run_kibana_server'; export { runElasticsearch } from './run_elasticsearch'; -export { runFtr } from './run_ftr'; +export { runFtr, hasTests, assertNoneExcluded } from './run_ftr'; export { KIBANA_ROOT, KIBANA_FTR_SCRIPT, FUNCTIONAL_CONFIG_PATH, API_CONFIG_PATH } from './paths'; export { runCli } from './run_cli'; diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 779286212f5c746..05e5cbb3d4b55cd 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -17,14 +17,11 @@ * under the License. */ -import { createFunctionalTestRunner } from '../../../../../src/functional_test_runner'; +import * as FunctionalTestRunner from '../../../../../src/functional_test_runner'; import { CliError } from './run_cli'; -export async function runFtr({ - configPath, - options: { log, bail, grep, updateBaselines, suiteTags }, -}) { - const ftr = createFunctionalTestRunner({ +function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) { + return FunctionalTestRunner.createFunctionalTestRunner({ log, configFile: configPath, configOverrides: { @@ -36,6 +33,27 @@ export async function runFtr({ suiteTags, }, }); +} + +export async function assertNoneExcluded({ configPath, options }) { + const ftr = createFtr({ configPath, options }); + + const stats = await ftr.getTestStats(); + if (stats.excludedTests.length > 0) { + throw new CliError(` + ${stats.excludedTests.length} tests in the ${configPath} config + are excluded when filtering by the tags run on CI. Make sure that all suites are + tagged with one of the following tags, or extend the list of tags in test/scripts/jenkins_xpack.sh + + tags: ${JSON.stringify(options.suiteTags)} + + - ${stats.excludedTests.join('\n - ')} + `); + } +} + +export async function runFtr({ configPath, options }) { + const ftr = createFtr({ configPath, options }); const failureCount = await ftr.run(); if (failureCount > 0) { @@ -44,3 +62,9 @@ export async function runFtr({ ); } } + +export async function hasTests({ configPath, options }) { + const ftr = createFtr({ configPath, options }); + const stats = await ftr.getTestStats(); + return stats.testCount > 0; +} diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 0cdcc77161a60fa..b7f22836ab769ec 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -17,12 +17,19 @@ * under the License. */ -import { relative, resolve } from 'path'; +import { relative } from 'path'; import * as Rx from 'rxjs'; import { startWith, switchMap, take } from 'rxjs/operators'; import { withProcRunner } from '@kbn/dev-utils'; -import { runElasticsearch, runKibanaServer, runFtr, KIBANA_FTR_SCRIPT } from './lib'; +import { + runElasticsearch, + runKibanaServer, + runFtr, + assertNoneExcluded, + hasTests, + KIBANA_FTR_SCRIPT, +} from './lib'; import { readConfigFile } from '../../../../src/functional_test_runner/lib'; @@ -38,37 +45,63 @@ in another terminal session by running this command from this directory: /** * Run servers and tests for each config * @param {object} options Optional - * @property {string[]} configPaths Array of paths to configs - * @property {function} options.createLogger Optional logger creation function + * @property {string[]} options.configs Array of paths to configs + * @property {function} options.log An instance of the ToolingLog * @property {string} options.installDir Optional installation dir from which to run Kibana * @property {boolean} options.bail Whether to exit test run at the first failure * @property {string} options.esFrom Optionally run from source instead of snapshot */ export async function runTests(options) { for (const configPath of options.configs) { - await runSingleConfig(resolve(process.cwd(), configPath), options); + const log = options.createLogger(); + const opts = { + ...options, + log, + }; + + log.info('Running', configPath); + log.indent(2); + + if (options.assertNoneExcluded) { + await assertNoneExcluded({ configPath, options: opts }); + continue; + } + + if (!(await hasTests({ configPath, options: opts }))) { + log.info('Skipping', configPath, 'since all tests are excluded'); + continue; + } + + await withProcRunner(log, async procs => { + const config = await readConfigFile(log, configPath); + + const es = await runElasticsearch({ config, options: opts }); + await runKibanaServer({ procs, config, options: opts }); + await runFtr({ configPath, options: opts }); + + await procs.stop('kibana'); + await es.cleanup(); + }); } } /** * Start only servers using single config * @param {object} options Optional - * @property {string} options.configPath Path to a config file - * @property {function} options.createLogger Optional logger creation function + * @property {string} options.config Path to a config file + * @property {function} options.log An instance of the ToolingLog * @property {string} options.installDir Optional installation dir from which to run Kibana * @property {string} options.esFrom Optionally run from source instead of snapshot */ export async function startServers(options) { - const { config: configOption, createLogger } = options; - const configPath = resolve(process.cwd(), configOption); - const log = createLogger(); + const log = options.createLogger(); const opts = { ...options, log, }; await withProcRunner(log, async procs => { - const config = await readConfigFile(log, configPath); + const config = await readConfigFile(log, options.config); const es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ @@ -100,25 +133,3 @@ async function silence(milliseconds, { log }) { ) .toPromise(); } - -/* - * Start servers and run tests for single config - */ -async function runSingleConfig(configPath, options) { - const log = options.createLogger(); - const opts = { - ...options, - log, - }; - - await withProcRunner(log, async procs => { - const config = await readConfigFile(log, configPath); - - const es = await runElasticsearch({ config, options: opts }); - await runKibanaServer({ procs, config, options: opts }); - await runFtr({ configPath, options: opts }); - - await procs.stop('kibana'); - await es.cleanup(); - }); -} diff --git a/scripts/register_git_hook.js b/scripts/register_git_hook.js new file mode 100644 index 000000000000000..8e03f17967f3fa0 --- /dev/null +++ b/scripts/register_git_hook.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +require('../src/setup_node_env'); +require('../src/dev/run_register_git_hook'); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 5840f826ba125e5..caaa588c2c9f8a5 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -202,19 +202,62 @@ Array [ "baz", ], ] +`); + }); + }); + + describe('breadcrumbs', () => { + it('updates/emits the current set of breadcrumbs', async () => { + const service = new ChromeService(); + const start = service.start(); + const promise = start + .getBreadcrumbs$() + .pipe(toArray()) + .toPromise(); + + start.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }]); + start.setBreadcrumbs([{ text: 'foo' }]); + start.setBreadcrumbs([{ text: 'bar' }]); + start.setBreadcrumbs([]); + service.stop(); + + await expect(promise).resolves.toMatchInlineSnapshot(` +Array [ + Array [], + Array [ + Object { + "text": "foo", + }, + Object { + "text": "bar", + }, + ], + Array [ + Object { + "text": "foo", + }, + ], + Array [ + Object { + "text": "bar", + }, + ], + Array [], +] `); }); }); }); describe('stop', () => { - it('completes applicationClass$, isCollapsed$, isVisible$, and brand$ observables', async () => { + it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => { const service = new ChromeService(); const start = service.start(); const promise = Rx.combineLatest( start.getBrand$(), start.getApplicationClasses$(), start.getIsCollapsed$(), + start.getBreadcrumbs$(), start.getIsVisible$() ).toPromise(); @@ -232,6 +275,7 @@ describe('stop', () => { start.getBrand$(), start.getApplicationClasses$(), start.getIsCollapsed$(), + start.getBreadcrumbs$(), start.getIsVisible$() ).toPromise() ).resolves.toBe(undefined); diff --git a/src/core/public/chrome/chrome_service.ts b/src/core/public/chrome/chrome_service.ts index 1e634aa42e2d867..8695385c9d20c24 100644 --- a/src/core/public/chrome/chrome_service.ts +++ b/src/core/public/chrome/chrome_service.ts @@ -34,6 +34,11 @@ export interface Brand { smallLogo?: string; } +export interface Breadcrumb { + text: string; + href?: string; +} + export class ChromeService { private readonly stop$ = new Rx.ReplaySubject(1); @@ -44,6 +49,7 @@ export class ChromeService { const isVisible$ = new Rx.BehaviorSubject(true); const isCollapsed$ = new Rx.BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY)); const applicationClasses$ = new Rx.BehaviorSubject>(new Set()); + const breadcrumbs$ = new Rx.BehaviorSubject([]); return { /** @@ -135,6 +141,18 @@ export class ChromeService { map(set => [...set]), takeUntil(this.stop$) ), + + /** + * Get an observable of the current list of breadcrumbs + */ + getBreadcrumbs$: () => breadcrumbs$.pipe(takeUntil(this.stop$)), + + /** + * Override the current set of breadcrumbs + */ + setBreadcrumbs: (newBreadcrumbs: Breadcrumb[]) => { + breadcrumbs$.next(newBreadcrumbs); + }, }; } diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index afc3d237ececbf8..ac54469e20bd4e9 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { ChromeService, ChromeStartContract, Brand } from './chrome_service'; +export { Breadcrumb, ChromeService, ChromeStartContract, Brand } from './chrome_service'; diff --git a/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap b/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap index b4a3fb8eface6ca..a1474127605dd7a 100644 --- a/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap +++ b/src/core/public/legacy_platform/__snapshots__/legacy_platform_service.test.ts.snap @@ -11,6 +11,7 @@ Array [ "ui/chrome/api/injected_vars", "ui/chrome/api/controls", "ui/chrome/api/theme", + "ui/chrome/api/breadcrumbs", "ui/chrome/services/global_nav_state", "ui/chrome", "legacy files", @@ -28,6 +29,7 @@ Array [ "ui/chrome/api/injected_vars", "ui/chrome/api/controls", "ui/chrome/api/theme", + "ui/chrome/api/breadcrumbs", "ui/chrome/services/global_nav_state", "ui/test_harness", "legacy files", diff --git a/src/core/public/legacy_platform/legacy_platform_service.test.ts b/src/core/public/legacy_platform/legacy_platform_service.test.ts index 8abf529a2a6bf11..957731fb7bcbf04 100644 --- a/src/core/public/legacy_platform/legacy_platform_service.test.ts +++ b/src/core/public/legacy_platform/legacy_platform_service.test.ts @@ -110,6 +110,14 @@ jest.mock('ui/chrome/api/theme', () => { }; }); +const mockChromeBreadcrumbsInit = jest.fn(); +jest.mock('ui/chrome/api/breadcrumbs', () => { + mockLoadOrder.push('ui/chrome/api/breadcrumbs'); + return { + __newPlatformInit__: mockChromeBreadcrumbsInit, + }; +}); + const mockGlobalNavStateInit = jest.fn(); jest.mock('ui/chrome/services/global_nav_state', () => { mockLoadOrder.push('ui/chrome/services/global_nav_state'); @@ -272,6 +280,17 @@ describe('#start()', () => { expect(mockChromeThemeInit).toHaveBeenCalledWith(chromeStartContract); }); + it('passes chrome service to ui/chrome/api/breadcrumbs', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.start(defaultStartDeps); + + expect(mockChromeBreadcrumbsInit).toHaveBeenCalledTimes(1); + expect(mockChromeBreadcrumbsInit).toHaveBeenCalledWith(chromeStartContract); + }); + it('passes chrome service to ui/chrome/api/global_nav_state', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, diff --git a/src/core/public/legacy_platform/legacy_platform_service.ts b/src/core/public/legacy_platform/legacy_platform_service.ts index 8354b9592f840a5..54bb912614cb20a 100644 --- a/src/core/public/legacy_platform/legacy_platform_service.ts +++ b/src/core/public/legacy_platform/legacy_platform_service.ts @@ -72,6 +72,7 @@ export class LegacyPlatformService { require('ui/chrome/api/injected_vars').__newPlatformInit__(injectedMetadata); require('ui/chrome/api/controls').__newPlatformInit__(chrome); require('ui/chrome/api/theme').__newPlatformInit__(chrome); + require('ui/chrome/api/breadcrumbs').__newPlatformInit__(chrome); require('ui/chrome/services/global_nav_state').__newPlatformInit__(chrome); // Load the bootstrap module before loading the legacy platform files so that diff --git a/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap b/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap index f33bff56f5a6355..d30bdead88c35bb 100644 --- a/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap +++ b/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap @@ -3,23 +3,25 @@ exports[`#start() renders the GlobalToastList into the targetDomElement param 1`] = ` Array [ Array [ - + , + /> + ,
, diff --git a/src/core/public/notifications/toasts/toasts_service.tsx b/src/core/public/notifications/toasts/toasts_service.tsx index fd7f7411b863e58..fd2fd29ce978f01 100644 --- a/src/core/public/notifications/toasts/toasts_service.tsx +++ b/src/core/public/notifications/toasts/toasts_service.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Toast } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; import { GlobalToastList } from './global_toast_list'; import { ToastsStartContract } from './toasts_start_contract'; @@ -35,10 +36,12 @@ export class ToastsService { const toasts = new ToastsStartContract(); render( - toasts.remove(toast)} - toasts$={toasts.get$()} - />, + + toasts.remove(toast)} + toasts$={toasts.get$()} + /> + , this.params.targetDomElement ); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/ensure_es_version.js b/src/core_plugins/elasticsearch/lib/__tests__/ensure_es_version.js index 2d554979ed92366..07a4c5373d17f36 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/ensure_es_version.js +++ b/src/core_plugins/elasticsearch/lib/__tests__/ensure_es_version.js @@ -42,6 +42,11 @@ describe('plugins/elasticsearch', () => { }, url: esTestConfig.getUrl() } + }, + config() { + return { + get: sinon.stub() + }; } }; }); diff --git a/src/core_plugins/elasticsearch/lib/ensure_es_version.js b/src/core_plugins/elasticsearch/lib/ensure_es_version.js index d0f0046fe44e89c..85ad2adfab5b95f 100644 --- a/src/core_plugins/elasticsearch/lib/ensure_es_version.js +++ b/src/core_plugins/elasticsearch/lib/ensure_es_version.js @@ -23,6 +23,7 @@ */ import { forEach, get } from 'lodash'; +import { coerce } from 'semver'; import isEsCompatibleWithKibana from './is_es_compatible_with_kibana'; /** @@ -37,6 +38,7 @@ const lastWarnedNodesForServer = new WeakMap(); export function ensureEsVersion(server, kibanaVersion) { const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const isProd = server.config().get('env.prod'); server.log(['plugin', 'debug'], 'Checking Elasticsearch version'); return callWithInternalUser('nodes.info', { @@ -61,7 +63,11 @@ export function ensureEsVersion(server, kibanaVersion) { // It's acceptable if ES and Kibana versions are not the same so long as // they are not incompatible, but we should warn about it - if (esNode.version !== kibanaVersion) { + // In development we ignore, this can be expected when testing against snapshots + // or across version qualifiers + const exactMisMatch = esNode.version !== kibanaVersion; + const looseMismatch = coerce(esNode.version).version !== coerce(kibanaVersion).version; + if (isProd && exactMisMatch || looseMismatch) { warningNodes.push(esNode); } }); diff --git a/src/core_plugins/input_control_vis/public/register_vis.js b/src/core_plugins/input_control_vis/public/register_vis.js index 859ed40d9ed1ad2..c975c4bb1afa437 100644 --- a/src/core_plugins/input_control_vis/public/register_vis.js +++ b/src/core_plugins/input_control_vis/public/register_vis.js @@ -41,7 +41,7 @@ function InputControlVisProvider(Private) { defaultMessage: 'Create interactive controls for easy dashboard manipulation.' }), category: CATEGORY.OTHER, - stage: 'lab', + stage: 'experimental', requiresUpdateStatus: [Status.PARAMS, Status.TIME], feedbackMessage: defaultFeedbackMessage, visualization: VisController, diff --git a/src/core_plugins/interpreter/common/constants.js b/src/core_plugins/interpreter/common/constants.js new file mode 100644 index 000000000000000..a5751ee72e82659 --- /dev/null +++ b/src/core_plugins/interpreter/common/constants.js @@ -0,0 +1,21 @@ +/* + * 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 const SECURITY_AUTH_MESSAGE = 'Authentication failed'; +export const API_ROUTE = '/api/canvas'; diff --git a/src/core_plugins/interpreter/index.js b/src/core_plugins/interpreter/index.js new file mode 100644 index 000000000000000..273c8b8c37957a6 --- /dev/null +++ b/src/core_plugins/interpreter/index.js @@ -0,0 +1,41 @@ +/* + * 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 { resolve } from 'path'; +import init from './init'; +import { pathsRegistry } from '@kbn/interpreter/common/lib/paths_registry'; +import { pluginPaths } from './plugin_paths'; + +export default function (kibana) { + return new kibana.Plugin({ + id: 'interpreter', + require: ['kibana', 'elasticsearch'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + hacks: [ + 'plugins/interpreter/load_browser_plugins.js', + ], + }, + preInit: () => { + pathsRegistry.registerAll(pluginPaths); + }, + init, + }); +} + diff --git a/src/core_plugins/interpreter/init.js b/src/core_plugins/interpreter/init.js new file mode 100644 index 000000000000000..58385973ac93012 --- /dev/null +++ b/src/core_plugins/interpreter/init.js @@ -0,0 +1,41 @@ +/* + * 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 { routes } from './server/routes'; +import { functionsRegistry } from '@kbn/interpreter/common/lib/functions_registry'; +import { populateServerRegistries } from '@kbn/interpreter/server/server_registries'; + +export default function (server /*options*/) { + server.injectUiAppVars('canvas', () => { + const config = server.config(); + const basePath = config.get('server.basePath'); + const reportingBrowserType = config.get('xpack.reporting.capture.browser.type'); + + return { + kbnIndex: config.get('kibana.index'), + esShardTimeout: config.get('elasticsearch.shardTimeout'), + esApiVersion: config.get('elasticsearch.apiVersion'), + serverFunctions: functionsRegistry.toArray(), + basePath, + reportingBrowserType, + }; + }); + + populateServerRegistries(['serverFunctions', 'types']).then(() => routes(server)); +} diff --git a/src/core_plugins/interpreter/package.json b/src/core_plugins/interpreter/package.json new file mode 100644 index 000000000000000..3265dadd7fbfc2c --- /dev/null +++ b/src/core_plugins/interpreter/package.json @@ -0,0 +1,4 @@ +{ + "name": "interpreter", + "version": "kibana" +} diff --git a/src/core_plugins/interpreter/plugin_paths.js b/src/core_plugins/interpreter/plugin_paths.js new file mode 100644 index 000000000000000..ca44ce1a1f7b299 --- /dev/null +++ b/src/core_plugins/interpreter/plugin_paths.js @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; + +const dir = resolve(__dirname, '..', '..', '..'); + +export const pluginPaths = { + commonFunctions: resolve(dir, 'node_modules/@kbn/interpreter/plugin/functions/common'), + types: resolve(dir, 'node_modules/@kbn/interpreter/plugin/types'), +}; diff --git a/src/core_plugins/interpreter/public/load_browser_plugins.js b/src/core_plugins/interpreter/public/load_browser_plugins.js new file mode 100644 index 000000000000000..6322e8e340e451c --- /dev/null +++ b/src/core_plugins/interpreter/public/load_browser_plugins.js @@ -0,0 +1,30 @@ +/* + * 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 { populateBrowserRegistries } from '@kbn/interpreter/public/browser_registries'; +import { typesRegistry } from '@kbn/interpreter/common/lib/types_registry'; +import { functionsRegistry } from '@kbn/interpreter/common/lib/functions_registry'; + +const types = { + commonFunctions: functionsRegistry, + browserFunctions: functionsRegistry, + types: typesRegistry +}; + +populateBrowserRegistries(types); diff --git a/x-pack/plugins/canvas/server/lib/__tests__/create_handlers.js b/src/core_plugins/interpreter/server/lib/__tests__/create_handlers.js similarity index 83% rename from x-pack/plugins/canvas/server/lib/__tests__/create_handlers.js rename to src/core_plugins/interpreter/server/lib/__tests__/create_handlers.js index 9dbe0e413a1af10..9afe458c444a73a 100644 --- a/x-pack/plugins/canvas/server/lib/__tests__/create_handlers.js +++ b/src/core_plugins/interpreter/server/lib/__tests__/create_handlers.js @@ -1,12 +1,25 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 expect from 'expect.js'; import { createHandlers } from '../create_handlers'; -import { SECURITY_AUTH_MESSAGE } from '../../../common/lib/constants'; +import { SECURITY_AUTH_MESSAGE } from '../../../common/constants'; let securityMode = 'pass'; let isSecurityAvailable = true; diff --git a/x-pack/plugins/canvas/server/lib/create_handlers.js b/src/core_plugins/interpreter/server/lib/create_handlers.js similarity index 58% rename from x-pack/plugins/canvas/server/lib/create_handlers.js rename to src/core_plugins/interpreter/server/lib/create_handlers.js index f42f8fbe1a59dc6..9c4dcd112c92865 100644 --- a/x-pack/plugins/canvas/server/lib/create_handlers.js +++ b/src/core_plugins/interpreter/server/lib/create_handlers.js @@ -1,12 +1,25 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 boom from 'boom'; -import { SECURITY_AUTH_MESSAGE } from '../../common/lib/constants'; import { isSecurityEnabled } from './feature_check'; +import { SECURITY_AUTH_MESSAGE } from '../../common/constants'; export const createHandlers = (request, server) => { const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); @@ -24,8 +37,9 @@ export const createHandlers = (request, server) => { if (isSecurityEnabled(server)) { try { const authenticationResult = await server.plugins.security.authenticate(request); - if (!authenticationResult.succeeded()) + if (!authenticationResult.succeeded()) { throw boom.unauthorized(authenticationResult.error); + } } catch (e) { // if authenticate throws, show error in development if (process.env.NODE_ENV !== 'production') { diff --git a/src/core_plugins/interpreter/server/lib/feature_check.js b/src/core_plugins/interpreter/server/lib/feature_check.js new file mode 100644 index 000000000000000..9f7a8993fa3ff64 --- /dev/null +++ b/src/core_plugins/interpreter/server/lib/feature_check.js @@ -0,0 +1,26 @@ +/* + * 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. + */ + +// TODO: replace this when we use the method exposed by security https://github.com/elastic/kibana/pull/24616 +export const isSecurityEnabled = server => { + const kibanaSecurity = server.plugins.security; + const esSecurity = server.plugins.xpack_main.info.feature('security'); + + return kibanaSecurity && esSecurity.isAvailable() && esSecurity.isEnabled(); +}; diff --git a/src/core_plugins/interpreter/server/lib/get_plugin_stream.js b/src/core_plugins/interpreter/server/lib/get_plugin_stream.js new file mode 100644 index 000000000000000..d685d365d31a403 --- /dev/null +++ b/src/core_plugins/interpreter/server/lib/get_plugin_stream.js @@ -0,0 +1,37 @@ +/* + * 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 fs from 'fs'; +import ss from 'stream-stream'; +import { getPluginPaths } from '@kbn/interpreter/server/get_plugin_paths'; + +export const getPluginStream = type => { + const stream = ss({ + separator: '\n', + }); + + getPluginPaths(type).then(files => { + files.forEach(file => { + stream.write(fs.createReadStream(file)); + }); + stream.end(); + }); + + return stream; +}; diff --git a/src/core_plugins/interpreter/server/lib/get_request.js b/src/core_plugins/interpreter/server/lib/get_request.js new file mode 100644 index 000000000000000..2b29b05fd07aab6 --- /dev/null +++ b/src/core_plugins/interpreter/server/lib/get_request.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import boom from 'boom'; +import { API_ROUTE } from '../../common/constants'; + +export function getRequest(server, { headers }) { + const url = `${API_ROUTE}/ping`; + + return server + .inject({ + method: 'POST', + url, + headers, + }) + .then(res => { + if (res.statusCode !== 200) { + if (process.env.NODE_ENV !== 'production') { + console.error( + new Error(`Auth request failed: [${res.statusCode}] ${res.result.message}`) + ); + } + throw boom.unauthorized('Failed to authenticate socket connection'); + } + + return res.request; + }); +} diff --git a/x-pack/plugins/canvas/server/lib/route_expression/browser.js b/src/core_plugins/interpreter/server/lib/route_expression/browser.js similarity index 65% rename from x-pack/plugins/canvas/server/lib/route_expression/browser.js rename to src/core_plugins/interpreter/server/lib/route_expression/browser.js index feae107873ac622..0fe27f4d27c684e 100644 --- a/x-pack/plugins/canvas/server/lib/route_expression/browser.js +++ b/src/core_plugins/interpreter/server/lib/route_expression/browser.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 uuid from 'uuid/v4'; diff --git a/x-pack/plugins/canvas/server/lib/route_expression/index.js b/src/core_plugins/interpreter/server/lib/route_expression/index.js similarity index 51% rename from x-pack/plugins/canvas/server/lib/route_expression/index.js rename to src/core_plugins/interpreter/server/lib/route_expression/index.js index 3533b55687246c4..1b3556e051d2df1 100644 --- a/x-pack/plugins/canvas/server/lib/route_expression/index.js +++ b/src/core_plugins/interpreter/server/lib/route_expression/index.js @@ -1,9 +1,23 @@ /* - * 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. + * 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 { createError } from '../../../common/interpreter/create_error'; + +import { createError } from '@kbn/interpreter/common/interpreter/create_error'; export const routeExpressionProvider = environments => { async function routeExpression(ast, context = null) { diff --git a/src/core_plugins/interpreter/server/lib/route_expression/server.js b/src/core_plugins/interpreter/server/lib/route_expression/server.js new file mode 100644 index 000000000000000..50a80a1e0275a45 --- /dev/null +++ b/src/core_plugins/interpreter/server/lib/route_expression/server.js @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getServerRegistries } from '@kbn/interpreter/server/server_registries'; +import { interpretProvider } from '@kbn/interpreter/common/interpreter/interpret'; +import { createHandlers } from '../create_handlers'; + +export const server = async ({ onFunctionNotFound, server, request }) => { + const { serverFunctions, types } = await getServerRegistries(['serverFunctions', 'types']); + + return { + interpret: (ast, context) => { + const interpret = interpretProvider({ + types: types.toJS(), + functions: serverFunctions.toJS(), + handlers: createHandlers(request, server), + onFunctionNotFound, + }); + + return interpret(ast, context); + }, + getFunctions: () => Object.keys(serverFunctions.toJS()), + }; +}; diff --git a/src/core_plugins/interpreter/server/lib/route_expression/thread/babeled.js b/src/core_plugins/interpreter/server/lib/route_expression/thread/babeled.js new file mode 100644 index 000000000000000..2a19ef81d135e9a --- /dev/null +++ b/src/core_plugins/interpreter/server/lib/route_expression/thread/babeled.js @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('babel-register')({ + ignore: [ + // stolen from kibana/src/setup_node_env/babel_register/register.js + // ignore paths matching `/node_modules/{a}/{b}`, unless `a` + // is `x-pack` and `b` is not `node_modules` + /\/node_modules\/(?!x-pack\/(?!node_modules)([^\/]+))([^\/]+\/[^\/]+)/, + ], + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/node_preset')], +}); + +require('./polyfill'); +require('./worker'); diff --git a/x-pack/plugins/canvas/server/lib/route_expression/thread/index.js b/src/core_plugins/interpreter/server/lib/route_expression/thread/index.js similarity index 78% rename from x-pack/plugins/canvas/server/lib/route_expression/thread/index.js rename to src/core_plugins/interpreter/server/lib/route_expression/thread/index.js index d3748db02f65c34..ff476793325e90b 100644 --- a/x-pack/plugins/canvas/server/lib/route_expression/thread/index.js +++ b/src/core_plugins/interpreter/server/lib/route_expression/thread/index.js @@ -1,7 +1,20 @@ /* - * 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. + * 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 { fork } from 'child_process'; diff --git a/src/core_plugins/interpreter/server/lib/route_expression/thread/polyfill.js b/src/core_plugins/interpreter/server/lib/route_expression/thread/polyfill.js new file mode 100644 index 000000000000000..476777b4bc69325 --- /dev/null +++ b/src/core_plugins/interpreter/server/lib/route_expression/thread/polyfill.js @@ -0,0 +1,31 @@ +/* + * 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. + */ + +// taken from kibana/src/setup_node_env/babel_register/polyfill.js +// ... +// `babel-preset-env` looks for and rewrites the following import +// statement into a list of import statements based on the polyfills +// necessary for our target environment (the current version of node) +// but since it does that during compilation, `import 'babel-polyfill'` +// must be in a file that is loaded with `require()` AFTER `babel-register` +// is configured. +// +// This is why we have this single statement in it's own file and require +// it from ./babeled.js +import 'babel-polyfill'; diff --git a/x-pack/plugins/canvas/server/lib/route_expression/thread/worker.js b/src/core_plugins/interpreter/server/lib/route_expression/thread/worker.js similarity index 62% rename from x-pack/plugins/canvas/server/lib/route_expression/thread/worker.js rename to src/core_plugins/interpreter/server/lib/route_expression/thread/worker.js index d81df410f7af79c..5159679bb9f4f71 100644 --- a/x-pack/plugins/canvas/server/lib/route_expression/thread/worker.js +++ b/src/core_plugins/interpreter/server/lib/route_expression/thread/worker.js @@ -1,13 +1,26 @@ /* - * 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. + * 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 uuid from 'uuid/v4'; -import { populateServerRegistries } from '../../server_registries'; -import { interpretProvider } from '../../../../common/interpreter/interpret'; -import { serializeProvider } from '../../../../common/lib/serialize'; +import { populateServerRegistries } from '@kbn/interpreter/server/server_registries'; +import { interpretProvider } from '@kbn/interpreter/common/interpreter/interpret'; +import { serializeProvider } from '@kbn/interpreter/common/lib/serialize'; // We actually DO need populateServerRegistries here since this is a different node process const pluginsReady = populateServerRegistries(['commonFunctions', 'types']); @@ -44,8 +57,9 @@ process.on('message', msg => { }, }); - if (type === 'getFunctions') + if (type === 'getFunctions') { process.send({ type: 'functionList', value: Object.keys(commonFunctions.toJS()) }); + } if (type === 'msgSuccess') { heap[id].resolve(deserialize(value)); diff --git a/src/core_plugins/interpreter/server/routes/index.js b/src/core_plugins/interpreter/server/routes/index.js new file mode 100644 index 000000000000000..f78baf4ad496d9f --- /dev/null +++ b/src/core_plugins/interpreter/server/routes/index.js @@ -0,0 +1,28 @@ +/* + * 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 { socketApi } from './socket'; +import { translate } from './translate'; +import { plugins } from './plugins'; + +export function routes(server) { + plugins(server); + socketApi(server); + translate(server); +} diff --git a/src/core_plugins/interpreter/server/routes/plugins.js b/src/core_plugins/interpreter/server/routes/plugins.js new file mode 100644 index 000000000000000..3d8c8614cc10759 --- /dev/null +++ b/src/core_plugins/interpreter/server/routes/plugins.js @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getPluginStream } from '../lib/get_plugin_stream'; + +export function plugins(server) { + server.route({ + method: 'GET', + path: '/api/canvas/plugins', + handler: function (request) { + const { type } = request.query; + + return getPluginStream(type); + }, + config: { + auth: false, + }, + }); +} diff --git a/x-pack/plugins/canvas/server/routes/socket.js b/src/core_plugins/interpreter/server/routes/socket.js similarity index 68% rename from x-pack/plugins/canvas/server/routes/socket.js rename to src/core_plugins/interpreter/server/routes/socket.js index 8e06c25769d4c19..daf16ec7a443244 100644 --- a/x-pack/plugins/canvas/server/routes/socket.js +++ b/src/core_plugins/interpreter/server/routes/socket.js @@ -1,19 +1,32 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 socket from 'socket.io'; -import { serializeProvider } from '../../common/lib/serialize'; -import { typesRegistry } from '../../common/lib/types_registry'; -import { getServerRegistries } from '../lib/server_registries'; -import { routeExpressionProvider } from '../lib/route_expression'; +import { serializeProvider } from '@kbn/interpreter/common/lib/serialize'; +import { typesRegistry } from '@kbn/interpreter/common/lib/types_registry'; +import { getServerRegistries } from '@kbn/interpreter/server/server_registries'; +import { routeExpressionProvider } from '../lib/route_expression/index'; import { browser } from '../lib/route_expression/browser'; -import { thread } from '../lib/route_expression/thread'; +import { thread } from '../lib/route_expression/thread/index'; import { server as serverEnv } from '../lib/route_expression/server'; import { getRequest } from '../lib/get_request'; -import { API_ROUTE } from '../../common/lib/constants'; +import { API_ROUTE } from '../../common/constants'; async function getModifiedRequest(server, socket) { try { diff --git a/src/core_plugins/interpreter/server/routes/translate.js b/src/core_plugins/interpreter/server/routes/translate.js new file mode 100644 index 000000000000000..865c0da3e061714 --- /dev/null +++ b/src/core_plugins/interpreter/server/routes/translate.js @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fromExpression, toExpression } from '@kbn/interpreter/common/lib/ast'; + +export function translate(server) { + /* + Get AST from expression + */ + server.route({ + method: 'GET', + path: '/api/canvas/ast', + handler: function (request, h) { + if (!request.query.expression) { + return h.response({ error: '"expression" query is required' }).code(400); + } + return fromExpression(request.query.expression); + }, + }); + + server.route({ + method: 'POST', + path: '/api/canvas/expression', + handler: function (request, h) { + try { + return toExpression(request.payload); + } catch (e) { + return h.response({ error: e.message }).code(400); + } + }, + }); +} diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js index cabd27767b2cb7c..3f97601422d7640 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js @@ -136,10 +136,13 @@ module.directive('vislibValueAxes', function () { }, 1); }; - const lastAxisTitles = {}; + const lastCustomLabels = {}; + // We track these so we can know when the agg is changed + let lastMatchingSeriesAggType = ''; + let lastMatchingSeriesAggField = ''; $scope.updateAxisTitle = function () { $scope.editorState.params.valueAxes.forEach((axis, axisNumber) => { - let label = ''; + let newCustomLabel = ''; const isFirst = axisNumber === 0; const matchingSeries = []; $scope.editorState.params.seriesParams.forEach((series, i) => { @@ -154,13 +157,31 @@ module.directive('vislibValueAxes', function () { }); } }); + if (matchingSeries.length === 1) { - label = matchingSeries[0].makeLabel(); + newCustomLabel = matchingSeries[0].makeLabel(); } - if (lastAxisTitles[axis.id] !== label && label !== '') { - lastAxisTitles[axis.id] = label; - axis.title.text = label; + + const matchingSeriesAggType = _.get(matchingSeries, '[0]type.name', ''); + const matchingSeriesAggField = _.get(matchingSeries, '[0]params.field.name', ''); + + if (lastCustomLabels[axis.id] !== newCustomLabel && newCustomLabel !== '') { + const isFirstRender = Object.keys(lastCustomLabels).length === 0; + const aggTypeIsChanged = lastMatchingSeriesAggType !== matchingSeriesAggType; + const aggFieldIsChanged = lastMatchingSeriesAggField !== matchingSeriesAggField; + const aggIsChanged = aggTypeIsChanged || aggFieldIsChanged; + const axisTitleIsEmpty = axis.title.text === ''; + const lastCustomLabelMatchesAxisTitle = lastCustomLabels[axis.id] === axis.title.text; + + if (!isFirstRender && (aggIsChanged || axisTitleIsEmpty || lastCustomLabelMatchesAxisTitle)) { + axis.title.text = newCustomLabel; // Override axis title with new custom label + } + + lastCustomLabels[axis.id] = newCustomLabel; } + + lastMatchingSeriesAggType = matchingSeriesAggType; + lastMatchingSeriesAggField = matchingSeriesAggField; }); }; diff --git a/src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js b/src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js index a4384ae2a1fea97..be20e8952492e91 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js @@ -136,4 +136,41 @@ describe('point series editor', function () { $parentScope.updateAxisTitle(); expect($parentScope.editorState.params.valueAxes[0].title.text).to.equal('Custom Title'); }); + + it('should set the custom title to match the value axis label when only one agg exists for that axis', function () { + $parentScope.editorState.aggs[0].params.customLabel = 'Custom Label'; + $parentScope.updateAxisTitle(); + expect($parentScope.editorState.params.valueAxes[0].title.text).to.equal('Custom Label'); + }); + + it('should not set the custom title to match the value axis label when more than one agg exists for that axis', function () { + const aggConfig = new AggConfig($parentScope.vis.aggs, { type: 'avg', schema: 'metric', params: { field: 'bytes' } }); + $parentScope.vis.aggs.push(aggConfig); + $parentScope.$digest(); + $parentScope.editorState.aggs[0].params.customLabel = 'Custom Label'; + $parentScope.updateAxisTitle(); + expect($parentScope.editorState.params.valueAxes[0].title.text).to.equal('Count'); + }); + + it('should not overwrite the custom title with the value axis label if the custom title has been changed', function () { + $parentScope.editorState.params.valueAxes[0].title.text = 'Custom Title'; + $parentScope.editorState.aggs[0].params.customLabel = 'Custom Label'; + $parentScope.updateAxisTitle(); + expect($parentScope.editorState.params.valueAxes[0].title.text).to.equal('Custom Title'); + }); + + it('should overwrite the custom title when the agg type changes', function () { + const aggConfig = new AggConfig($parentScope.vis.aggs, { type: 'avg', schema: 'metric', params: { field: 'bytes' } }); + + $parentScope.editorState.params.valueAxes[0].title.text = 'Custom Title'; + $parentScope.editorState.aggs[0].params.customLabel = 'Custom Label'; + $parentScope.updateAxisTitle(); + + $parentScope.vis.aggs.push(aggConfig); + $parentScope.vis.aggs.shift(); + $parentScope.$digest(); + $parentScope.updateAxisTitle(); + + expect($parentScope.editorState.params.valueAxes[0].title.text).to.equal('Average bytes'); + }); }); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/gauge.js b/src/core_plugins/kbn_vislib_vis_types/public/gauge.js index d89ffabfc5711c6..708e2afff7265d6 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/gauge.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/gauge.js @@ -80,6 +80,9 @@ export default function GaugeVisType(Private, i18n) { } }, }, + events: { + brush: { disabled: true }, + }, editorConfig: { collections: { gaugeTypes: ['Arc', 'Circle'], diff --git a/src/core_plugins/kbn_vislib_vis_types/public/goal.js b/src/core_plugins/kbn_vislib_vis_types/public/goal.js index 78e6b9e17e61f7f..0826df5c2de9838 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/goal.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/goal.js @@ -76,6 +76,9 @@ export default function GoalVisType(Private, i18n) { } }, }, + events: { + brush: { disabled: true }, + }, editorConfig: { collections: { gaugeTypes: ['Arc', 'Circle'], diff --git a/src/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/core_plugins/kbn_vislib_vis_types/public/pie.js index 59f62deb0641e58..366cce5e2c939e1 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -46,6 +46,9 @@ export default function HistogramVisType(Private, i18n) { } }, }, + events: { + brush: { disabled: true }, + }, editorConfig: { collections: { legendPositions: [{ diff --git a/src/core_plugins/kibana/public/dashboard/_index.scss b/src/core_plugins/kibana/public/dashboard/_index.scss index de00e0d670d22a8..98e918e507d9215 100644 --- a/src/core_plugins/kibana/public/dashboard/_index.scss +++ b/src/core_plugins/kibana/public/dashboard/_index.scss @@ -41,8 +41,7 @@ @import 'panel/index'; @import 'viewport/index'; - // Vis imports -- will have some duplicate styling - // because they will be imported via ui/public as well - // (without .theme-[] prefix) - @import 'src/ui/public/vis/map/index'; + // Vis imports + @import 'src/ui/public/vis/index'; + @import 'src/ui/public/vislib/index'; } diff --git a/src/core_plugins/kibana/public/dashboard/components/__snapshots__/exit_full_screen_button.test.js.snap b/src/core_plugins/kibana/public/dashboard/components/__snapshots__/exit_full_screen_button.test.js.snap index e41e0bf21420de0..2fe29dd29a59ba0 100644 --- a/src/core_plugins/kibana/public/dashboard/components/__snapshots__/exit_full_screen_button.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/components/__snapshots__/exit_full_screen_button.test.js.snap @@ -28,7 +28,7 @@ exports[`is rendered 1`] = ` class="dshExitFullScreenButton__text" data-test-subj="exitFullScreenModeText" > - Exit full screen + Exit full screen diff --git a/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.js b/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.js index 1d95ff0d09aac48..d24d3a81017ae34 100644 --- a/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.js +++ b/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.js @@ -20,6 +20,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import chrome from 'ui/chrome'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KuiButton, @@ -30,7 +31,7 @@ import { EuiScreenReaderOnly, } from '@elastic/eui'; -export class ExitFullScreenButton extends PureComponent { +class ExitFullScreenButtonUi extends PureComponent { onKeyDown = (e) => { if (e.keyCode === keyCodes.ESCAPE) { @@ -49,11 +50,16 @@ export class ExitFullScreenButton extends PureComponent { } render() { + const { intl } = this.props; + return (

- In full screen mode, press ESC to exit. +

- Exit full screen + +
@@ -76,6 +89,8 @@ export class ExitFullScreenButton extends PureComponent { } } -ExitFullScreenButton.propTypes = { +ExitFullScreenButtonUi.propTypes = { onExitFullScreenMode: PropTypes.func.isRequired, }; + +export const ExitFullScreenButton = injectI18n(ExitFullScreenButtonUi); diff --git a/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.test.js b/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.test.js index 0fb74f1a29ff99a..80a52584cf9ba49 100644 --- a/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.test.js +++ b/src/core_plugins/kibana/public/dashboard/components/exit_full_screen_button.test.js @@ -24,7 +24,7 @@ jest.mock('ui/chrome', }), { virtual: true }); import React from 'react'; -import { render, mount } from 'enzyme'; +import { mountWithIntl, renderWithIntl } from 'test_utils/enzyme_helpers'; import sinon from 'sinon'; import chrome from 'ui/chrome'; @@ -36,8 +36,8 @@ import { keyCodes } from '@elastic/eui'; test('is rendered', () => { - const component = render( - {}}/> + const component = renderWithIntl( + {}}/> ); expect(component) @@ -48,8 +48,8 @@ describe('onExitFullScreenMode', () => { test('is called when the button is pressed', () => { const onExitHandler = sinon.stub(); - const component = mount( - + const component = mountWithIntl( + ); component.find('button').simulate('click'); @@ -60,7 +60,7 @@ describe('onExitFullScreenMode', () => { test('is called when the ESC key is pressed', () => { const onExitHandler = sinon.stub(); - mount(); + mountWithIntl(); const escapeKeyEvent = new KeyboardEvent('keydown', { keyCode: keyCodes.ESCAPE }); document.dispatchEvent(escapeKeyEvent); @@ -73,8 +73,8 @@ describe('chrome.setVisible', () => { test('is called with false when the component is rendered', () => { chrome.setVisible = sinon.stub(); - const component = mount( - {}} /> + const component = mountWithIntl( + {}} /> ); component.find('button').simulate('click'); @@ -84,8 +84,8 @@ describe('chrome.setVisible', () => { }); test('is called with true the component is unmounted', () => { - const component = mount( - {}} /> + const component = mountWithIntl( + {}} /> ); chrome.setVisible = sinon.stub(); diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.html b/src/core_plugins/kibana/public/dashboard/dashboard_app.html index cc020f33e3dba27..18c40bda026f4e3 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -15,7 +15,12 @@ aria-level="1" ng-if="showPluginBreadcrumbs">
- Dashboard +
{{ getDashTitle() }} @@ -46,22 +51,64 @@ ng-show="getShouldShowEditHelp()" class="dshStartScreen" > -

- This dashboard is empty. Let’s fill it up! +

-

- Click the Add button in the menu bar above to add a visualization to the dashboard.
If you haven't set up any visualizations yet, visit the Visualize app to create your first visualization. +

+ + +

-

- This dashboard is empty. Let’s fill it up! +

- Click the Edit button in the menu bar above to start working on your new dashboard. + + +

diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 13f6c1006c95611..919191885a43307 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -56,7 +56,6 @@ import { timefilter } from 'ui/timefilter'; import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; -import { i18n } from '@kbn/i18n'; const app = uiModules.get('app/dashboard', [ 'elasticsearch', @@ -86,11 +85,10 @@ app.directive('dashboardApp', function ($injector) { $rootScope, $route, $routeParams, - $location, getAppState, dashboardConfig, localStorage, - breadcrumbState + i18n, ) { const filterManager = Private(FilterManagerProvider); const filterBar = Private(FilterBarQueryFilterProvider); @@ -182,9 +180,9 @@ app.directive('dashboardApp', function ($injector) { // Push breadcrumbs to new header navigation const updateBreadcrumbs = () => { - breadcrumbState.set([ + chrome.breadcrumbs.set([ { - text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', { + text: i18n('kbn.dashboard.dashboardAppBreadcrumbsTitle', { defaultMessage: 'Dashboard', }), href: $scope.landingPageUrl() @@ -273,14 +271,22 @@ app.directive('dashboardApp', function ($injector) { } confirmModal( - `Once you discard your changes, there's no getting them back.`, + i18n('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', + { defaultMessage: `Once you discard your changes, there's no getting them back.` } + ), { onConfirm: revertChangesAndExitEditMode, onCancel: _.noop, - confirmButtonText: 'Discard changes', - cancelButtonText: 'Continue editing', + confirmButtonText: i18n('kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', + { defaultMessage: 'Discard changes' } + ), + cancelButtonText: i18n('kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', + { defaultMessage: 'Continue editing' } + ), defaultFocusedButton: ConfirmationButtonTypes.CANCEL, - title: 'Discard changes to dashboard?' + title: i18n('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', + { defaultMessage: 'Discard changes to dashboard?' } + ) } ); }; @@ -302,7 +308,12 @@ app.directive('dashboardApp', function ($injector) { .then(function (id) { if (id) { toastNotifications.addSuccess({ - title: `Dashboard '${dash.title}' was saved`, + title: i18n('kbn.dashboard.dashboardWasSavedSuccessMessage', + { + defaultMessage: `Dashboard '{dashTitle}' was saved`, + values: { dashTitle: dash.title }, + }, + ), 'data-test-subj': 'saveDashboardSuccess', }); @@ -316,7 +327,15 @@ app.directive('dashboardApp', function ($injector) { return { id }; }).catch((error) => { toastNotifications.addDanger({ - title: `Dashboard '${dash.title}' was not saved. Error: ${error.message}`, + title: i18n('kbn.dashboard.dashboardWasNotSavedDangerMessage', + { + defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`, + values: { + dashTitle: dash.title, + errorMessage: error.message, + }, + }, + ), 'data-test-subj': 'saveDashboardFailure', }); return { error }; diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js b/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js index a9a9372318592fb..aea6ba3b964261e 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import moment from 'moment'; @@ -550,7 +551,9 @@ export class DashboardStateManager { */ syncTimefilterWithDashboard(timeFilter, quickTimeRanges) { if (!this.getIsTimeSavedWithDashboard()) { - throw new Error('The time is not saved with this dashboard so should not be synced.'); + throw new Error(i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', { + defaultMessage: 'The time is not saved with this dashboard so should not be synced.', + })); } let mode; diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_strings.js b/src/core_plugins/kibana/public/dashboard/dashboard_strings.js index 7dffc94c9e97d14..a0d2af3e9c00a67 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_strings.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_strings.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { DashboardViewMode } from './dashboard_view_mode'; /** @@ -28,10 +29,21 @@ import { DashboardViewMode } from './dashboard_view_mode'; */ export function getDashboardTitle(title, viewMode, isDirty) { const isEditMode = viewMode === DashboardViewMode.EDIT; - const unsavedSuffix = isEditMode && isDirty - ? ' (unsaved)' - : ''; + let displayTitle; - const displayTitle = `${title}${unsavedSuffix}`; - return isEditMode ? 'Editing ' + displayTitle : displayTitle; + if (isEditMode && isDirty) { + displayTitle = i18n.translate('kbn.dashboard.strings.dashboardUnsavedEditTitle', { + defaultMessage: 'Editing {title} (unsaved)', + values: { title }, + }); + } else if (isEditMode) { + displayTitle = i18n.translate('kbn.dashboard.strings.dashboardEditTitle', { + defaultMessage: 'Editing {title}', + values: { title }, + }); + } else { + displayTitle = title; + } + + return displayTitle; } diff --git a/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap b/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap index a098c22af2363ed..806e11c557a0254 100644 --- a/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap @@ -33,7 +33,7 @@ exports[`renders DashboardGrid 1`] = ` } } > - - { }); test('renders DashboardGrid', () => { - const component = shallow(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); - const panelElements = component.find('Connect(DashboardPanel)'); + const panelElements = component.find('Connect(InjectIntl(DashboardPanelUi))'); expect(panelElements.length).toBe(2); }); test('renders DashboardGrid with no visualizations', () => { - const component = shallow(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); test('adjusts z-index of focused panel to be higher than siblings', () => { - const component = shallow(); - const panelElements = component.find('Connect(DashboardPanel)'); + const component = shallowWithIntl(); + const panelElements = component.find('Connect(InjectIntl(DashboardPanelUi))'); panelElements.first().prop('onPanelFocused')('1'); const [gridItem1, gridItem2] = component.update().findWhere(el => el.key() === '1' || el.key() === '2'); expect(gridItem1.props.style.zIndex).toEqual('2'); diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.test.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.test.js index 1a7e3db04868317..12d5d42c811ffd1 100644 --- a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.test.js +++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.test.js @@ -18,7 +18,7 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { Provider } from 'react-redux'; import _ from 'lodash'; import sizeMe from 'react-sizeme'; @@ -94,7 +94,7 @@ test('loads old panel data in the right order', () => { store.dispatch(updatePanels(panelData)); store.dispatch(updateUseMargins(false)); - const grid = mount(); + const grid = mountWithIntl(); const panels = store.getState().dashboard.panels; expect(Object.keys(panels).length).toBe(16); @@ -130,7 +130,7 @@ test('loads old panel data in the right order with margins', () => { store.dispatch(updatePanels(panelData)); store.dispatch(updateUseMargins(true)); - const grid = mount(); + const grid = mountWithIntl(); const panels = store.getState().dashboard.panels; expect(Object.keys(panels).length).toBe(16); diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index 3c302ec41bb67ca..72280b7bc9d715a 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -17,10 +17,12 @@ * under the License. */ +import { injectI18nProvider } from '@kbn/i18n/react'; import './dashboard_app'; import './saved_dashboard/saved_dashboards'; import './dashboard_config'; import uiRoutes from 'ui/routes'; +import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import dashboardTemplate from './dashboard_app.html'; @@ -34,7 +36,6 @@ import { recentlyAccessed } from 'ui/persisted_log'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { uiModules } from 'ui/modules'; -import { i18n } from '@kbn/i18n'; const app = uiModules.get('app/dashboard', [ 'ngRoute', @@ -42,16 +43,22 @@ const app = uiModules.get('app/dashboard', [ ]); app.directive('dashboardListing', function (reactDirective) { - return reactDirective(DashboardListing); + return reactDirective(injectI18nProvider(DashboardListing)); }); +function createNewDashboardCtrl($scope, i18n) { + $scope.visitVisualizeAppLinkText = i18n('kbn.dashboard.visitVisualizeAppLinkText', { + defaultMessage: 'visit the Visualize app', + }); +} + uiRoutes .defaults(/dashboard/, { requireDefaultIndex: true }) .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, - controller($injector, $location, $scope, Private, config, breadcrumbState) { + controller($injector, $location, $scope, Private, config, i18n) { const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; const dashboardConfig = $injector.get('dashboardConfig'); @@ -64,8 +71,8 @@ uiRoutes }; $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); $scope.initialFilter = ($location.search()).filter || EMPTY_FILTER; - breadcrumbState.set([{ - text: i18n.translate('kbn.dashboard.dashboardBreadcrumbsTitle', { + chrome.breadcrumbs.set([{ + text: i18n('kbn.dashboard.dashboardBreadcrumbsTitle', { defaultMessage: 'Dashboards', }), }]); @@ -98,6 +105,7 @@ uiRoutes }) .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, { template: dashboardTemplate, + controller: createNewDashboardCtrl, resolve: { dash: function (savedDashboards, redirectWhenMissing) { return savedDashboards.get() @@ -109,8 +117,9 @@ uiRoutes }) .when(createDashboardEditUrl(':id'), { template: dashboardTemplate, + controller: createNewDashboardCtrl, resolve: { - dash: function (savedDashboards, Notifier, $route, $location, redirectWhenMissing, kbnUrl, AppState) { + dash: function (savedDashboards, Notifier, $route, $location, redirectWhenMissing, kbnUrl, AppState, i18n) { const id = $route.current.params.id; return savedDashboards.get(id) @@ -131,7 +140,9 @@ uiRoutes if (error instanceof SavedObjectNotFound && id === 'create') { // Note "new AppState" is necessary so the state in the url is preserved through the redirect. kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState()); - toastNotifications.addWarning('The url "dashboard/create" was removed in 6.0. Please update your bookmarks.'); + toastNotifications.addWarning(i18n('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', + { defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.' } + )); } else { throw error; } @@ -143,11 +154,15 @@ uiRoutes } }); -FeatureCatalogueRegistryProvider.register(() => { +FeatureCatalogueRegistryProvider.register((i18n) => { return { id: 'dashboard', - title: 'Dashboard', - description: 'Display and share a collection of visualizations and saved searches.', + title: i18n('kbn.dashboard.featureCatalogue.dashboardTitle', { + defaultMessage: 'Dashboard', + }), + description: i18n('kbn.dashboard.featureCatalogue.dashboardDescription', { + defaultMessage: 'Display and share a collection of visualizations and saved searches.', + }), icon: 'dashboardApp', path: `/app/kibana#${DashboardConstants.LANDING_PAGE_PATH}`, showOnHomePage: true, diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index cd45b2ae64b103d..e350b43c4028f7c 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -22,7 +22,11 @@ exports[`after fetch hideWriteControls 1`] = ` color="subdued" component="span" > - Looks like you don't have any dashboards. + @@ -64,7 +68,11 @@ exports[`after fetch initialFilter 1`] = ` textTransform="none" >

- Dashboards +

@@ -80,7 +88,11 @@ exports[`after fetch initialFilter 1`] = ` iconSide="left" type="button" > - Create new dashboard + @@ -108,7 +120,7 @@ exports[`after fetch initialFilter 1`] = ` incremental={false} isLoading={false} onChange={[Function]} - placeholder="Search..." + placeholder="Search…" value="my dashboard" /> @@ -157,7 +169,13 @@ exports[`after fetch initialFilter 1`] = ` ] } loading={false} - noItemsMessage="No dashboards matched your search." + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { @@ -210,24 +228,42 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` iconType="plusInCircle" type="button" > - Create new dashboard + } body={

- You can combine data views from any Kibana app into one dashboard and see everything in one place. +

- New to Kibana? - - Install some sample data - - to take a test drive. + + + , + } + } + />

} @@ -235,7 +271,11 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` iconType="dashboardApp" title={

- Create your first dashboard +

} /> @@ -278,7 +318,11 @@ exports[`after fetch renders table rows 1`] = ` textTransform="none" >

- Dashboards +

@@ -294,7 +338,11 @@ exports[`after fetch renders table rows 1`] = ` iconSide="left" type="button" > - Create new dashboard + @@ -322,7 +370,7 @@ exports[`after fetch renders table rows 1`] = ` incremental={false} isLoading={false} onChange={[Function]} - placeholder="Search..." + placeholder="Search…" value="" /> @@ -371,7 +419,13 @@ exports[`after fetch renders table rows 1`] = ` ] } loading={false} - noItemsMessage="No dashboards matched your search." + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { @@ -432,7 +486,11 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` textTransform="none" >

- Dashboards +

@@ -448,7 +506,11 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` iconSide="left" type="button" > - Create new dashboard + @@ -460,26 +522,39 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` color="warning" iconType="help" size="m" - title="Listing limit exceeded" + title={ + + } >

- You have - 2 - dashboards, but your - - listingLimit - - setting prevents the table below from displaying more than - 1 - . You can change this setting under - - Advanced Settings - - . + + + , + "listingLimitText": + listingLimit + , + "listingLimitValue": 1, + "totalDashboards": 2, + } + } + />

@@ -556,7 +631,13 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` ] } loading={false} - noItemsMessage="No dashboards matched your search." + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index 2a2aef320880ad2..b7af6a616ba261e 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -19,6 +19,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import { toastNotifications } from 'ui/notify'; import { @@ -49,7 +50,7 @@ export const EMPTY_FILTER = ''; // and not supporting server-side paging. // This component does not try to tackle these problems (yet) and is just feature matching the legacy component // TODO support server side sorting/paging once title and description are sortable on the server. -export class DashboardListing extends React.Component { +class DashboardListingUi extends React.Component { constructor(props) { super(props); @@ -111,7 +112,12 @@ export class DashboardListing extends React.Component { await this.props.delete(this.state.selectedIds); } catch (error) { toastNotifications.addDanger({ - title: `Unable to delete dashboard(s)`, + title: ( + + ), text: `${error}`, }); } @@ -194,14 +200,34 @@ export class DashboardListing extends React.Component { return ( + } onCancel={this.closeDeleteModal} onConfirm={this.deleteSelectedItems} - cancelButtonText="Cancel" - confirmButtonText="Delete" + cancelButtonText={ + + } + confirmButtonText={ + + } defaultFocusedButton="cancel" > -

{`You can't recover deleted dashboards.`}

+

+ +

); @@ -212,14 +238,38 @@ export class DashboardListing extends React.Component { return ( + } color="warning" iconType="help" >

- You have {this.state.totalDashboards} dashboards, - but your listingLimit setting prevents the table below from displaying more than {this.props.listingLimit}. - You can change this setting under Advanced Settings. + + listingLimit + + ), + advancedSettingsLink: ( + + + + ) + }} + />

@@ -233,7 +283,12 @@ export class DashboardListing extends React.Component { return ''; } - return 'No dashboards matched your search.'; + return ( + + ); } renderNoItemsMessage() { @@ -243,7 +298,10 @@ export class DashboardListing extends React.Component {

- {`Looks like you don't have any dashboards.`} +

@@ -254,14 +312,37 @@ export class DashboardListing extends React.Component {
Create your first dashboard} + title={ +

+ +

+ } body={

- You can combine data views from any Kibana app into one dashboard and see everything in one place. +

- New to Kibana? Install some sample data to take a test drive. + + + + ), + }} + />

} @@ -272,7 +353,10 @@ export class DashboardListing extends React.Component { iconType="plusInCircle" data-test-subj="createDashboardPromptButton" > - Create new dashboard + } /> @@ -282,6 +366,7 @@ export class DashboardListing extends React.Component { } renderSearchBar() { + const { intl } = this.props; let deleteBtn; if (this.state.selectedIds.length > 0) { deleteBtn = ( @@ -292,7 +377,10 @@ export class DashboardListing extends React.Component { data-test-subj="deleteSelectedDashboards" key="delete" > - Delete selected + ); @@ -303,8 +391,14 @@ export class DashboardListing extends React.Component { {deleteBtn} { @@ -320,10 +414,14 @@ export class DashboardListing extends React.Component { } renderTable() { + const { intl } = this.props; const tableColumns = [ { field: 'title', - name: 'Title', + name: intl.formatMessage({ + id: 'kbn.dashboard.listing.table.titleColumnName', + defaultMessage: 'Title', + }), sortable: true, render: (field, record) => ( { @@ -351,7 +455,10 @@ export class DashboardListing extends React.Component { - Edit + ); } @@ -413,7 +520,10 @@ export class DashboardListing extends React.Component { href={`#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`} data-test-subj="newDashboardLink" > - Create new dashboard + ); @@ -426,7 +536,10 @@ export class DashboardListing extends React.Component {

- Dashboards +

@@ -471,7 +584,7 @@ export class DashboardListing extends React.Component { } } -DashboardListing.propTypes = { +DashboardListingUi.propTypes = { find: PropTypes.func.isRequired, delete: PropTypes.func.isRequired, listingLimit: PropTypes.number.isRequired, @@ -479,6 +592,8 @@ DashboardListing.propTypes = { initialFilter: PropTypes.string, }; -DashboardListing.defaultProps = { +DashboardListingUi.defaultProps = { initialFilter: EMPTY_FILTER, }; + +export const DashboardListing = injectI18n(DashboardListingUi); diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js index e1a95b65982a6ad..9fcb0c6bab90b2d 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js @@ -36,7 +36,7 @@ jest.mock('lodash', }), { virtual: true }); import React from 'react'; -import { shallow } from 'enzyme'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DashboardListing, @@ -58,7 +58,7 @@ const find = (num) => { }; test('renders empty page in before initial fetch to avoid flickering', () => { - const component = shallow( {}} listingLimit={1000} @@ -69,7 +69,7 @@ test('renders empty page in before initial fetch to avoid flickering', () => { describe('after fetch', () => { test('initialFilter', async () => { - const component = shallow( {}} listingLimit={1000} @@ -86,7 +86,7 @@ describe('after fetch', () => { }); test('renders table rows', async () => { - const component = shallow( {}} listingLimit={1000} @@ -102,7 +102,7 @@ describe('after fetch', () => { }); test('renders call to action when no dashboards exist', async () => { - const component = shallow( {}} listingLimit={1} @@ -118,7 +118,7 @@ describe('after fetch', () => { }); test('hideWriteControls', async () => { - const component = shallow( {}} listingLimit={1} @@ -134,7 +134,7 @@ describe('after fetch', () => { }); test('renders warning when listingLimit is exceeded', async () => { - const component = shallow( {}} listingLimit={1} diff --git a/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss b/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss index 6efb9d4f25b0de1..70a4b0e76a669f5 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss +++ b/src/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss @@ -35,7 +35,7 @@ @include euiScrollBar; /* 3 */ } - .visualization .vis-container { + .visualization .visChart__container { overflow: visible; /* 2 */ } } diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js index 684fe6c54a223ee..1ec58d19715417f 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js +++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js @@ -19,6 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import classNames from 'classnames'; import _ from 'lodash'; @@ -29,11 +30,14 @@ import { EuiPanel, } from '@elastic/eui'; -export class DashboardPanel extends React.Component { +class DashboardPanelUi extends React.Component { constructor(props) { super(props); this.state = { - error: props.embeddableFactory ? null : `No factory found for embeddable`, + error: props.embeddableFactory ? null : props.intl.formatMessage({ + id: 'kbn.dashboard.panel.noEmbeddableFactoryErrorMessage', + defaultMessage: 'No factory found for embeddable', + }), }; this.mounted = false; @@ -100,7 +104,10 @@ export class DashboardPanel extends React.Component { className="panel-content" ref={panelElement => this.panelElement = panelElement} > - {!this.props.initialized && 'loading...'} + {!this.props.initialized && }
); } @@ -151,7 +158,7 @@ export class DashboardPanel extends React.Component { } } -DashboardPanel.propTypes = { +DashboardPanelUi.propTypes = { viewOnlyMode: PropTypes.bool.isRequired, onPanelFocused: PropTypes.func, onPanelBlurred: PropTypes.func, @@ -179,3 +186,5 @@ DashboardPanel.propTypes = { panelIndex: PropTypes.string, }).isRequired, }; + +export const DashboardPanel = injectI18n(DashboardPanelUi); diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js index 5f142a8b1e33834..eb8258f43833b1a 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js +++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js @@ -19,7 +19,7 @@ import React from 'react'; import _ from 'lodash'; -import { mount } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DashboardPanel } from './dashboard_panel'; import { DashboardViewMode } from '../dashboard_view_mode'; import { PanelError } from '../panel/panel_error'; @@ -62,7 +62,7 @@ beforeAll(() => { }); test('DashboardPanel matches snapshot', () => { - const component = mount(); + const component = mountWithIntl(); expect(takeMountedSnapshot(component)).toMatchSnapshot(); }); @@ -71,7 +71,7 @@ test('renders an error when error prop is passed', () => { error: 'Simulated error' }); - const component = mount(); + const component = mountWithIntl(); const panelError = component.find(PanelError); expect(panelError.length).toBe(1); }); diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.js index 1b62a93f2cfb827..556c81d32f8d216 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.js +++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.js @@ -19,6 +19,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { i18n } from '@kbn/i18n'; import { DashboardPanel } from './dashboard_panel'; import { DashboardViewMode } from '../dashboard_view_mode'; @@ -40,7 +41,10 @@ const mapStateToProps = ({ dashboard }, { embeddableFactory, panelId }) => { let error = null; if (!embeddableFactory) { const panelType = getPanelType(dashboard, panelId); - error = `No embeddable factory found for panel type ${panelType}`; + error = i18n.translate('kbn.dashboard.panel.noFoundEmbeddableFactoryErrorMessage', { + defaultMessage: 'No embeddable factory found for panel type {panelType}', + values: { panelType }, + }); } else { error = (embeddable && getEmbeddableError(dashboard, panelId)) || ''; } diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.test.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.test.js index 2af88510b715212..cb532f1a48e38a7 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.test.js +++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.test.js @@ -19,7 +19,7 @@ import React from 'react'; import _ from 'lodash'; -import { mount } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DashboardPanelContainer } from './dashboard_panel_container'; import { DashboardViewMode } from '../dashboard_view_mode'; import { PanelError } from '../panel/panel_error'; @@ -52,7 +52,7 @@ test('renders an error when embeddableFactory.create throws an error', (done) => throw new Error('simulated error'); }); }; - const component = mount(); + const component = mountWithIntl(); setTimeout(() => { component.update(); const panelError = component.find(PanelError); diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx index f7cd97666b5c31f..dd059c47475c361 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx @@ -18,6 +18,7 @@ */ import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import { ContextMenuAction, ContextMenuPanel } from 'ui/embeddable'; import { DashboardViewMode } from '../../../dashboard_view_mode'; @@ -36,7 +37,9 @@ export function getCustomizePanelAction({ }): ContextMenuAction { return new ContextMenuAction( { - displayName: 'Customize panel', + displayName: i18n.translate('kbn.dashboard.panel.customizePanel.displayName', { + defaultMessage: 'Customize panel', + }), id: 'customizePanel', parentPanelId: 'mainMenu', }, @@ -44,7 +47,9 @@ export function getCustomizePanelAction({ childContextMenuPanel: new ContextMenuPanel( { id: 'panelSubOptionsMenu', - title: 'Customize panel', + title: i18n.translate('kbn.dashboard.panel.customizePanelTitle', { + defaultMessage: 'Customize panel', + }), }, { getContent: () => ( diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx index 88bc0001a9ac1b5..f4413fc5539e788 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ContextMenuAction } from 'ui/embeddable'; import { DashboardViewMode } from '../../../dashboard_view_mode'; @@ -31,7 +32,9 @@ import { DashboardViewMode } from '../../../dashboard_view_mode'; export function getEditPanelAction() { return new ContextMenuAction( { - displayName: 'Edit visualization', + displayName: i18n.translate('kbn.dashboard.panel.editPanel.displayName', { + defaultMessage: 'Edit visualization', + }), id: 'editPanel', parentPanelId: 'mainMenu', }, diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx index 2739e2859a42962..01f6da6421109a5 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ContextMenuAction } from 'ui/embeddable'; import { Inspector } from 'ui/inspector'; @@ -41,7 +42,9 @@ export function getInspectorPanelAction({ return new ContextMenuAction( { id: 'openInspector', - displayName: 'Inspect', + displayName: i18n.translate('kbn.dashboard.panel.inspectorPanel.displayName', { + defaultMessage: 'Inspect', + }), parentPanelId: 'mainMenu', }, { diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx index fce94f24b16ce9a..47718a5f21b314f 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx @@ -18,6 +18,7 @@ */ import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import { ContextMenuAction } from 'ui/embeddable'; @@ -31,7 +32,9 @@ import { DashboardViewMode } from '../../../dashboard_view_mode'; export function getRemovePanelAction(onDeletePanel: () => void) { return new ContextMenuAction( { - displayName: 'Delete from dashboard', + displayName: i18n.translate('kbn.dashboard.panel.removePanel.displayName', { + defaultMessage: 'Delete from dashboard', + }), id: 'deletePanel', parentPanelId: 'mainMenu', }, diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx index 27dca29c01ba67e..80d43347b308cad 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx @@ -18,6 +18,7 @@ */ import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import { ContextMenuAction } from 'ui/embeddable'; @@ -37,7 +38,13 @@ export function getToggleExpandPanelAction({ }) { return new ContextMenuAction( { - displayName: isExpanded ? 'Minimize' : 'Full screen', + displayName: isExpanded + ? i18n.translate('kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName', { + defaultMessage: 'Minimize', + }) + : i18n.translate('kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName', { + defaultMessage: 'Full screen', + }), id: 'togglePanel', parentPanelId: 'mainMenu', }, diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header.tsx index c0acd5a672a653d..9e9e59d79aa9792 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header.tsx @@ -17,6 +17,7 @@ * under the License. */ +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; import { Embeddable } from 'ui/embeddable'; import { PanelId } from '../../selectors'; @@ -30,13 +31,18 @@ export interface PanelHeaderProps { hidePanelTitles: boolean; } -export function PanelHeader({ +interface PanelHeaderUiProps extends PanelHeaderProps { + intl: InjectedIntl; +} + +function PanelHeaderUi({ title, panelId, embeddable, isViewOnlyMode, hidePanelTitles, -}: PanelHeaderProps) { + intl, +}: PanelHeaderUiProps) { if (isViewOnlyMode && (!title || hidePanelTitles)) { return (
@@ -56,7 +62,15 @@ export function PanelHeader({ data-test-subj="dashboardPanelTitle" className="dshPanel__title" title={title} - aria-label={`Dashboard panel: ${title}`} + aria-label={intl.formatMessage( + { + id: 'kbn.dashboard.panel.dashboardPanelAriaLabel', + defaultMessage: 'Dashboard panel: {title}', + }, + { + title, + } + )} > {hidePanelTitles ? '' : title} @@ -67,3 +81,5 @@ export function PanelHeader({
); } + +export const PanelHeader = injectI18n(PanelHeaderUi); diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.test.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.test.tsx index 998c1f6dc3ffc41..f395b17207be5e2 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.test.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.test.tsx @@ -17,10 +17,11 @@ * under the License. */ -import { mount, ReactWrapper } from 'enzyme'; +import { ReactWrapper } from 'enzyme'; import _ from 'lodash'; import React from 'react'; import { Provider } from 'react-redux'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; // TODO: remove this when EUI supports types for this. // @ts-ignore: implicit any for JS file @@ -77,7 +78,7 @@ afterAll(() => { }); test('Panel header shows embeddable title when nothing is set on the panel', () => { - component = mount( + component = mountWithIntl( diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu.tsx index 983a235a719f21e..25efd58d059e181 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu.tsx @@ -17,6 +17,7 @@ * under the License. */ +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; import { @@ -34,19 +35,27 @@ export interface PanelOptionsMenuProps { isViewMode: boolean; } -export function PanelOptionsMenu({ +interface PanelOptionsMenuUiProps extends PanelOptionsMenuProps { + intl: InjectedIntl; +} + +function PanelOptionsMenuUi({ toggleContextMenu, isPopoverOpen, closeContextMenu, panels, isViewMode, -}: PanelOptionsMenuProps) { + intl, +}: PanelOptionsMenuUiProps) { const button = ( @@ -70,3 +79,5 @@ export function PanelOptionsMenu({ ); } + +export const PanelOptionsMenu = injectI18n(PanelOptionsMenuUi); diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts index 294a7dd2661ceb8..478c59847fe3a43 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts @@ -18,6 +18,7 @@ */ import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { connect } from 'react-redux'; import { buildEuiContextMenuPanels, @@ -167,7 +168,9 @@ const mergeProps = ( // every panel, every time any state changes. if (isPopoverOpen) { const contextMenuPanel = new ContextMenuPanel({ - title: 'Options', + title: i18n.translate('kbn.dashboard.panel.optionsMenu.optionsContextMenuTitle', { + defaultMessage: 'Options', + }), id: 'mainMenu', }); diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_form.tsx b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_form.tsx index 4b7b4a3a1341cd7..c80ab00a46b771e 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_form.tsx +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_form.tsx @@ -20,6 +20,7 @@ import React, { ChangeEvent, KeyboardEvent } from 'react'; import { EuiButtonEmpty, EuiFieldText, EuiFormRow, keyCodes } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; export interface PanelOptionsMenuFormProps { title?: string; @@ -28,12 +29,17 @@ export interface PanelOptionsMenuFormProps { onClose: () => void; } -export function PanelOptionsMenuForm({ +interface PanelOptionsMenuFormUiProps extends PanelOptionsMenuFormProps { + intl: InjectedIntl; +} + +function PanelOptionsMenuFormUi({ title, onReset, onUpdatePanelTitle, onClose, -}: PanelOptionsMenuFormProps) { + intl, +}: PanelOptionsMenuFormUiProps) { function onInputChange(event: ChangeEvent) { onUpdatePanelTitle(event.target.value); } @@ -46,7 +52,12 @@ export function PanelOptionsMenuForm({ return (
- + - Reset title +
); } + +export const PanelOptionsMenuForm = injectI18n(PanelOptionsMenuFormUi); diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js index c5b60f1018bb360..aec24998f17b70d 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js +++ b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../dashboard_constants'; import chrome from 'ui/chrome'; @@ -31,7 +32,10 @@ export class PanelUtils { static convertPanelDataPre_6_1(panel) { // eslint-disable-line camelcase ['col', 'row'].forEach(key => { if (!_.has(panel, key)) { - throw new Error(`Unable to migrate panel data for "6.1.0" backwards compatibility, panel does not contain expected field: ${key}`); + throw new Error(i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', { + defaultMessage: 'Unable to migrate panel data for "6.1.0" backwards compatibility, panel does not contain expected field: {key}', + values: { key }, + })); } }); @@ -59,7 +63,10 @@ export class PanelUtils { static convertPanelDataPre_6_3(panel, useMargins) { // eslint-disable-line camelcase ['w', 'x', 'h', 'y'].forEach(key => { if (!_.has(panel.gridData, key)) { - throw new Error(`Unable to migrate panel data for "6.3.0" backwards compatibility, panel does not contain expected field: ${key}`); + throw new Error(i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', { + defaultMessage: 'Unable to migrate panel data for "6.3.0" backwards compatibility, panel does not contain expected field: {key}', + values: { key }, + })); } }); @@ -78,7 +85,13 @@ export class PanelUtils { static parseVersion(version = '6.0.0') { const versionSplit = version.split('.'); if (versionSplit.length < 3) { - throw new Error(`Invalid version, ${version}, expected ..`); + throw new Error(i18n.translate('kbn.dashboard.panel.invalidVersionErrorMessage', { + defaultMessage: 'Invalid version, {version}, expected {semver}', + values: { + version, + semver: '..', + }, + })); } return { major: parseInt(versionSplit[0], 10), diff --git a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js index cf82b8274b633fb..04773be6e428b03 100644 --- a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js +++ b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js @@ -26,7 +26,7 @@ import { SavedObjectProvider } from 'ui/courier'; const module = uiModules.get('app/dashboard'); // Used only by the savedDashboards service, usually no reason to change this -module.factory('SavedDashboard', function (Private, config) { +module.factory('SavedDashboard', function (Private, config, i18n) { // SavedDashboard constructor. Usually you'd interact with an instance of this. // ID is option, without it one will be generated on save. const SavedObject = Private(SavedObjectProvider); @@ -43,7 +43,7 @@ module.factory('SavedDashboard', function (Private, config) { // default values that will get assigned if the doc is new defaults: { - title: 'New Dashboard', + title: i18n('kbn.dashboard.savedDashboard.newDashboardTitle', { defaultMessage: 'New Dashboard' }), hits: 0, description: '', panelsJSON: '[]', diff --git a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js index a4563b71a26fac5..c4ab8b9a96e3735 100644 --- a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js +++ b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import './saved_dashboard'; import { uiModules } from 'ui/modules'; import { SavedObjectLoader } from 'ui/courier/saved_object/saved_object_loader'; @@ -29,7 +30,9 @@ const module = uiModules.get('app/dashboard'); // edited by the object editor. savedObjectManagementRegistry.register({ service: 'savedDashboards', - title: 'dashboards' + title: i18n.translate('kbn.dashboard.savedDashboardsTitle', { + defaultMessage: 'dashboards', + }), }); // This is the only thing that gets injected into controllers diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap index d976372dd2bf388..4f96414b427ceee 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap @@ -16,7 +16,11 @@ exports[`render 1`] = ` textTransform="none" >

- Add Panels +

- Add new Visualization + } key="visSavedObjectFinder" diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap index 80fb7a7ed275c75..92e8f07ea0da4a2 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/clone_modal.test.js.snap @@ -10,7 +10,11 @@ exports[`renders DashboardCloneModal 1`] = ` > - Clone Dashboard + @@ -19,7 +23,11 @@ exports[`renders DashboardCloneModal 1`] = ` size="m" >

- Please enter a new name for your dashboard. +

- Cancel + - Confirm Clone + diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap index 2112aee0763cbb5..aae42c0b98ce0cb 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap @@ -11,7 +11,13 @@ exports[`renders DashboardSaveModal 1`] = ` describedByIds={Array []} fullWidth={false} hasEmptyLabelSpace={false} - label="Description" + label={ + + } > + } + label={ + + } > - Add new Visualization + ); const tabs = [{ id: VIS_TAB_ID, - name: 'Visualization', + name: props.intl.formatMessage({ + id: 'kbn.dashboard.topNav.addPanel.visualizationTabName', + defaultMessage: 'Visualization', + }), dataTestSubj: 'addVisualizationTab', toastDataTestSubj: 'addVisualizationToDashboardSuccess', savedObjectFinder: ( @@ -59,20 +66,29 @@ export class DashboardAddPanel extends React.Component { callToActionButton={addNewVisBtn} onChoose={this.onAddPanel} visTypes={this.props.visTypes} - noItemsMessage="No matching visualizations found." + noItemsMessage={props.intl.formatMessage({ + id: 'kbn.dashboard.topNav.addPanel.visSavedObjectFinder.noMatchingVisualizationsMessage', + defaultMessage: 'No matching visualizations found.', + })} savedObjectType="visualization" /> ) }, { id: SAVED_SEARCH_TAB_ID, - name: 'Saved Search', + name: props.intl.formatMessage({ + id: 'kbn.dashboard.topNav.addPanel.savedSearchTabName', + defaultMessage: 'Saved Search', + }), dataTestSubj: 'addSavedSearchTab', toastDataTestSubj: 'addSavedSearchToDashboardSuccess', savedObjectFinder: ( ) @@ -115,7 +131,12 @@ export class DashboardAddPanel extends React.Component { } this.lastToast = toastNotifications.addSuccess({ - title: `${this.state.selectedTab.name} was added to your dashboard`, + title: this.props.intl.formatMessage({ + id: 'kbn.dashboard.topNav.addPanel.selectedTabAddedToDashboardSuccessMessageTitle', + defaultMessage: '{selectedTabName} was added to your dashboard', + }, { + selectedTabName: this.state.selectedTab.name, + }), 'data-test-subj': this.state.selectedTab.toastDataTestSubj, }); } @@ -131,7 +152,12 @@ export class DashboardAddPanel extends React.Component { -

Add Panels

+

+ +

@@ -148,9 +174,11 @@ export class DashboardAddPanel extends React.Component { } } -DashboardAddPanel.propTypes = { +DashboardAddPanelUi.propTypes = { onClose: PropTypes.func.isRequired, visTypes: PropTypes.object.isRequired, addNewPanel: PropTypes.func.isRequired, addNewVis: PropTypes.func.isRequired, }; + +export const DashboardAddPanel = injectI18n(DashboardAddPanelUi); diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js b/src/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js index 9c17980f7b9cda7..3f233eed6b100bf 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js @@ -19,7 +19,7 @@ import React from 'react'; import sinon from 'sinon'; -import { shallow } from 'enzyme'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DashboardAddPanel, @@ -38,7 +38,7 @@ beforeEach(() => { }); test('render', () => { - const component = shallow( {}} diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js index 5bab97387a14f0a..507eb2b6db34e10 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js @@ -19,6 +19,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -34,7 +35,7 @@ import { EuiCallOut, } from '@elastic/eui'; -export class DashboardCloneModal extends React.Component { +class DashboardCloneModalUi extends React.Component { constructor(props) { super(props); @@ -90,12 +91,30 @@ export class DashboardCloneModal extends React.Component { return (

- Click Confirm Clone to clone the dashboard with the duplicate title. + + + + ), + }} + />

@@ -113,14 +132,20 @@ export class DashboardCloneModal extends React.Component { > - Clone Dashboard +

- Please enter a new name for your dashboard. +

@@ -143,7 +168,10 @@ export class DashboardCloneModal extends React.Component { data-test-subj="cloneCancelButton" onClick={this.props.onClose} > - Cancel + - Confirm Clone + @@ -161,8 +192,10 @@ export class DashboardCloneModal extends React.Component { } } -DashboardCloneModal.propTypes = { +DashboardCloneModalUi.propTypes = { onClone: PropTypes.func, onClose: PropTypes.func, title: PropTypes.string }; + +export const DashboardCloneModal = injectI18n(DashboardCloneModalUi); diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js index c8154fe3bdd6bcc..3dfaa26b7982673 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js @@ -19,7 +19,7 @@ import React from 'react'; import sinon from 'sinon'; -import { mount, shallow } from 'enzyme'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject, } from '@elastic/eui/lib/test'; @@ -37,9 +37,9 @@ beforeEach(() => { onClose = sinon.spy(); }); -function createComponent(creationMethod = mount) { +function createComponent(creationMethod = mountWithIntl) { component = creationMethod( - { - createComponent(shallow); + createComponent(shallowWithIntl); expect(component).toMatchSnapshot(); // eslint-disable-line }); diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js b/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js index c0ac1cb2702b2c4..969ff3f631de57b 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { DashboardViewMode } from '../dashboard_view_mode'; import { TopNavIds } from './top_nav_ids'; @@ -57,8 +58,12 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) { function getFullScreenConfig(action) { return { - key: 'full screen', - description: 'Full Screen Mode', + key: i18n.translate('kbn.dashboard.topNave.fullScreenButtonAriaLabel', { + defaultMessage: 'full screen', + }), + description: i18n.translate('kbn.dashboard.topNave.fullScreenConfigDescription', { + defaultMessage: 'Full Screen Mode', + }), testId: 'dashboardFullScreenMode', run: action }; @@ -69,8 +74,12 @@ function getFullScreenConfig(action) { */ function getEditConfig(action) { return { - key: 'edit', - description: 'Switch to edit mode', + key: i18n.translate('kbn.dashboard.topNave.editButtonAriaLabel', { + defaultMessage: 'edit', + }), + description: i18n.translate('kbn.dashboard.topNave.editConfigDescription', { + defaultMessage: 'Switch to edit mode', + }), testId: 'dashboardEditMode', run: action }; @@ -81,8 +90,12 @@ function getEditConfig(action) { */ function getSaveConfig(action) { return { - key: TopNavIds.SAVE, - description: 'Save your dashboard', + key: i18n.translate('kbn.dashboard.topNave.saveButtonAriaLabel', { + defaultMessage: 'save', + }), + description: i18n.translate('kbn.dashboard.topNave.saveConfigDescription', { + defaultMessage: 'Save your dashboard', + }), testId: 'dashboardSaveMenuItem', run: action }; @@ -93,8 +106,12 @@ function getSaveConfig(action) { */ function getViewConfig(action) { return { - key: 'cancel', - description: 'Cancel editing and switch to view-only mode', + key: i18n.translate('kbn.dashboard.topNave.cancelButtonAriaLabel', { + defaultMessage: 'cancel', + }), + description: i18n.translate('kbn.dashboard.topNave.viewConfigDescription', { + defaultMessage: 'Cancel editing and switch to view-only mode', + }), testId: 'dashboardViewOnlyMode', run: action }; @@ -105,8 +122,12 @@ function getViewConfig(action) { */ function getCloneConfig(action) { return { - key: TopNavIds.CLONE, - description: 'Create a copy of your dashboard', + key: i18n.translate('kbn.dashboard.topNave.cloneButtonAriaLabel', { + defaultMessage: 'clone', + }), + description: i18n.translate('kbn.dashboard.topNave.cloneConfigDescription', { + defaultMessage: 'Create a copy of your dashboard', + }), testId: 'dashboardClone', run: action }; @@ -117,8 +138,12 @@ function getCloneConfig(action) { */ function getAddConfig(action) { return { - key: TopNavIds.ADD, - description: 'Add a panel to the dashboard', + key: i18n.translate('kbn.dashboard.topNave.addButtonAriaLabel', { + defaultMessage: 'add', + }), + description: i18n.translate('kbn.dashboard.topNave.addConfigDescription', { + defaultMessage: 'Add a panel to the dashboard', + }), testId: 'dashboardAddPanelButton', run: action }; @@ -129,8 +154,12 @@ function getAddConfig(action) { */ function getShareConfig(action) { return { - key: TopNavIds.SHARE, - description: 'Share Dashboard', + key: i18n.translate('kbn.dashboard.topNave.shareButtonAriaLabel', { + defaultMessage: 'share', + }), + description: i18n.translate('kbn.dashboard.topNave.shareConfigDescription', { + defaultMessage: 'Share Dashboard', + }), testId: 'shareTopNavButton', run: action, }; @@ -141,8 +170,12 @@ function getShareConfig(action) { */ function getOptionsConfig(action) { return { - key: TopNavIds.OPTIONS, - description: 'Options', + key: i18n.translate('kbn.dashboard.topNave.optionsButtonAriaLabel', { + defaultMessage: 'options', + }), + description: i18n.translate('kbn.dashboard.topNave.optionsConfigDescription', { + defaultMessage: 'Options', + }), testId: 'dashboardOptionsButton', run: action, }; diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/options.js b/src/core_plugins/kibana/public/dashboard/top_nav/options.js index 6c42c2d26a25e01..62c3b65374db47c 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/options.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/options.js @@ -19,6 +19,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { injectI18n } from '@kbn/i18n/react'; import { EuiForm, @@ -26,7 +27,7 @@ import { EuiSwitch, } from '@elastic/eui'; -export class OptionsMenu extends Component { +class OptionsMenuUi extends Component { state = { darkTheme: this.props.darkTheme, @@ -60,7 +61,10 @@ export class OptionsMenu extends Component { } > } + helpText={} > { - const component = shallow( {}} onClose={() => {}} title="dash title" diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js b/src/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js index 00719a850aca421..d4d8b1a0e404058 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js @@ -17,6 +17,7 @@ * under the License. */ +import { I18nProvider } from '@kbn/i18n/react'; import { DashboardAddPanel } from './add_panel'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -43,12 +44,14 @@ export function showAddPanel(addNewPanel, addNewVis, visTypes) { document.body.appendChild(container); const element = ( - + + + ); ReactDOM.render(element, container); } diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.js b/src/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.js index d75c3da660e9ffe..08163a9a35956a6 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.js @@ -17,9 +17,11 @@ * under the License. */ +import { I18nProvider } from '@kbn/i18n/react'; import { DashboardCloneModal } from './clone_modal'; import React from 'react'; import ReactDOM from 'react-dom'; +import { i18n } from '@kbn/i18n'; export function showCloneModal(onClone, title) { const container = document.createElement('div'); @@ -37,7 +39,16 @@ export function showCloneModal(onClone, title) { }; document.body.appendChild(container); const element = ( - + + + ); ReactDOM.render(element, container); } diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js b/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js index 2d2dd83b4c34e99..cf6fba6316e763e 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js @@ -19,6 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; import { OptionsMenu } from './options'; @@ -53,22 +54,24 @@ export function showOptionsPopover({ document.body.appendChild(container); const element = ( - - - + + + + + ); ReactDOM.render(element, container); } diff --git a/src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_provider.js b/src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_provider.js index a219f763dac28cd..95b66cc1c4c59fb 100644 --- a/src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_provider.js +++ b/src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_provider.js @@ -19,6 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { I18nProvider } from '@kbn/i18n/react'; import { store } from '../../store'; import { Provider } from 'react-redux'; import { DashboardViewportContainer } from './dashboard_viewport_container'; @@ -26,7 +27,9 @@ import { DashboardViewportContainer } from './dashboard_viewport_container'; export function DashboardViewportProvider(props) { return ( - + + + ); } diff --git a/src/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js b/src/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js index 745693178d9669a..52dd71325a2a05b 100644 --- a/src/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js +++ b/src/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js @@ -21,6 +21,7 @@ import 'ngreact'; import React, { Fragment } from 'react'; import { uiModules } from 'ui/modules'; import chrome from 'ui/chrome'; +import { FormattedMessage, injectI18nProvider } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -43,9 +44,26 @@ const DiscoverFetchError = ({ fetchError }) => { body = (

- You can address this error by editing the ‘{fetchError.script}’ field - in Management > Index Patterns, - under the “Scripted fields” tab. + , + managementLink: ( + + + + ) + }} + />

); } @@ -77,4 +95,4 @@ const DiscoverFetchError = ({ fetchError }) => { const app = uiModules.get('apps/discover', ['react']); -app.directive('discoverFetchError', reactDirective => reactDirective(DiscoverFetchError)); +app.directive('discoverFetchError', reactDirective => reactDirective(injectI18nProvider(DiscoverFetchError))); diff --git a/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.html b/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.html index 76d7bec8e454cc3..206239f6ae3c4a1 100644 --- a/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.html +++ b/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.html @@ -17,7 +17,7 @@ ng-if="field.name !== '_source'" ng-click="toggleDisplay(field)" ng-class="::field.display ? 'kuiButton--danger' : 'kuiButton--primary'" - ng-bind="::field.display ? 'remove' : 'add'" + ng-bind="::addRemoveButtonLabel" class="dscSidebarItem__action kuiButton kuiButton--small" data-test-subj="fieldToggle-{{::field.name}}" > diff --git a/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js b/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js index 4627dfbdcf9e88d..8ff6e3a5751f671 100644 --- a/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js +++ b/src/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js @@ -26,7 +26,7 @@ import detailsHtml from './lib/detail_views/string.html'; import { uiModules } from 'ui/modules'; const app = uiModules.get('apps/discover'); -app.directive('discoverField', function ($compile) { +app.directive('discoverField', function ($compile, i18n) { return { restrict: 'E', template: html, @@ -42,11 +42,18 @@ app.directive('discoverField', function ($compile) { let detailsElem; let detailScope; - const init = function () { if ($scope.field.details) { $scope.toggleDetails($scope.field, true); } + + $scope.addRemoveButtonLabel = $scope.field.display + ? i18n('kbn.discover.fieldChooser.discoverField.removeButtonLabel', { + defaultMessage: 'remove', + }) + : i18n('kbn.discover.fieldChooser.discoverField.addButtonLabel', { + defaultMessage: 'add', + }); }; const getWarnings = function (field) { @@ -92,6 +99,18 @@ app.directive('discoverField', function ($compile) { $scope.onShowDetails(field, recompute); detailScope = $scope.$new(); detailScope.warnings = getWarnings(field); + detailScope.getBucketAriaLabel = (bucket) => { + return i18n('kbn.discover.fieldChooser.discoverField.bucketAriaLabel', { + defaultMessage: 'Value: {value}', + values: { + value: bucket.display === '' + ? i18n('kbn.discover.fieldChooser.discoverField.emptyStringText', { + defaultMessage: 'Empty string', + }) + : bucket.display, + }, + }); + }; detailsElem = $(detailsHtml); $compile(detailsElem)(detailScope); diff --git a/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html b/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html index 2e64db6656bca2a..73442a4ec74b8bb 100644 --- a/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html +++ b/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html @@ -1,4 +1,4 @@ -
diff --git a/src/core_plugins/kibana/public/discover/index.js b/src/core_plugins/kibana/public/discover/index.js index d1a0bb1c9f1e615..68685a9d5b28b8a 100644 --- a/src/core_plugins/kibana/public/discover/index.js +++ b/src/core_plugins/kibana/public/discover/index.js @@ -25,11 +25,15 @@ import './controllers/discover'; import 'ui/doc_table/components/table_row'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -FeatureCatalogueRegistryProvider.register(() => { +FeatureCatalogueRegistryProvider.register(i18n => { return { id: 'discover', - title: 'Discover', - description: 'Interactively explore your data by querying and filtering raw documents.', + title: i18n('kbn.discover.discoverTitle', { + defaultMessage: 'Discover', + }), + description: i18n('kbn.discover.discoverDescription', { + defaultMessage: 'Interactively explore your data by querying and filtering raw documents.', + }), icon: 'discoverApp', path: '/app/kibana#/discover', showOnHomePage: true, diff --git a/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js index 5a15c5a08c6bec0..89df75c75352057 100644 --- a/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ b/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js @@ -27,7 +27,7 @@ const module = uiModules.get('discover/saved_searches', [ 'kibana/courier' ]); -module.factory('SavedSearch', function (Private) { +module.factory('SavedSearch', function (Private, i18n) { const SavedObject = Private(SavedObjectProvider); createLegacyClass(SavedSearch).inherits(SavedObject); function SavedSearch(id) { @@ -38,7 +38,9 @@ module.factory('SavedSearch', function (Private) { id: id, defaults: { - title: 'New Saved Search', + title: i18n('kbn.discover.savedSearch.newSavedSearchTitle', { + defaultMessage: 'New Saved Search', + }), description: '', columns: [], hits: 0, diff --git a/src/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap index 4ddf453c5a8197a..05b8fae9ab1ddb6 100644 --- a/src/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap +++ b/src/core_plugins/kibana/public/discover/top_nav/__snapshots__/open_search_panel.test.js.snap @@ -16,7 +16,11 @@ exports[`render 1`] = ` textTransform="none" >

- Open Search +

- Manage searches + } makeUrl={[Function]} - noItemsMessage="No matching searches found." + noItemsMessage={ + + } onChoose={[Function]} savedObjectType="search" /> diff --git a/src/core_plugins/kibana/public/discover/top_nav/open_search_panel.js b/src/core_plugins/kibana/public/discover/top_nav/open_search_panel.js index f06dcf5d9c3d65d..1235272588fcb28 100644 --- a/src/core_plugins/kibana/public/discover/top_nav/open_search_panel.js +++ b/src/core_plugins/kibana/public/discover/top_nav/open_search_panel.js @@ -21,6 +21,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; import rison from 'rison-node'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, @@ -40,7 +41,10 @@ export class OpenSearchPanel extends React.Component { onClick={this.props.onClose} href={`#/management/kibana/objects?_a=${rison.encode({ tab: SEARCH_OBJECT_TYPE })}`} > - Manage searches + ); } @@ -55,13 +59,23 @@ export class OpenSearchPanel extends React.Component { -

Open Search

+

+ +

+ } savedObjectType={SEARCH_OBJECT_TYPE} makeUrl={this.props.makeUrl} onChoose={this.props.onClose} diff --git a/src/core_plugins/kibana/public/discover/top_nav/show_open_search_panel.js b/src/core_plugins/kibana/public/discover/top_nav/show_open_search_panel.js index 5b76f9ebc66527c..febfd5551491040 100644 --- a/src/core_plugins/kibana/public/discover/top_nav/show_open_search_panel.js +++ b/src/core_plugins/kibana/public/discover/top_nav/show_open_search_panel.js @@ -20,6 +20,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { OpenSearchPanel } from './open_search_panel'; +import { I18nProvider } from '@kbn/i18n/react'; let isOpen = false; @@ -38,10 +39,12 @@ export function showOpenSearchPanel({ makeUrl }) { document.body.appendChild(container); const element = ( - + + + ); ReactDOM.render(element, container); } diff --git a/src/core_plugins/kibana/public/index.scss b/src/core_plugins/kibana/public/index.scss index 390694b0a52c216..b415106ede3bf64 100644 --- a/src/core_plugins/kibana/public/index.scss +++ b/src/core_plugins/kibana/public/index.scss @@ -1,6 +1,7 @@ @import 'ui/public/styles/styling_constants'; -@import 'ui/public/query_bar/index'; +// Public UI styles +@import 'ui/public/index'; // Context styles @import './context/index'; @@ -16,6 +17,8 @@ // Visualize styles @import './visualize/index'; +// Has to come after visualize because of some +// bad cascading in the Editor layout @import 'ui/public/vis/index'; // Management styles diff --git a/src/core_plugins/kibana/public/management/_management_app.scss b/src/core_plugins/kibana/public/management/_management_app.scss index 8fbc3c27dec9d31..bd3cabbc574d359 100644 --- a/src/core_plugins/kibana/public/management/_management_app.scss +++ b/src/core_plugins/kibana/public/management/_management_app.scss @@ -3,11 +3,25 @@ background: $euiColorEmptyShade; } +/** + * 1. Override kuiPanelBody styles to accommodate padding of items within the panel body.. + */ +.mgtPanel__body { + padding: 5px 10px; /* 1 */ +} + +/** + * 1. Create vertical space between items when they wrap. + */ +.mgtPanel__item { + padding: 5px 15px; /* 1 */ +} + // SASSTODO: Remove when this is replaced by the side nav .mgtPanel__link { @include euiFontSizeL; - line-height: 2; // Give some buffer for when they wrap + line-height: 1.5; // Make sure the space between wrapped lines is than the vertical space between items. &.mgtPanel__link--disabled { opacity: $euiColorDarkShade; @@ -52,4 +66,4 @@ kbn-management-objects { display: inline-block; } } -} \ No newline at end of file +} diff --git a/src/core_plugins/kibana/public/management/landing.html b/src/core_plugins/kibana/public/management/landing.html index 7e0d81e7177a430..3171e6c3226fe73 100644 --- a/src/core_plugins/kibana/public/management/landing.html +++ b/src/core_plugins/kibana/public/management/landing.html @@ -21,11 +21,11 @@

-
+
@@ -466,17 +466,17 @@ i18n-id="timelion.help.expressions.paragraph4" i18n-default-message="Timelion provides additional view transformation functions you can use to customize the appearance of your charts. For the complete list, see the" - i18n-context="Part of composite text - timelion.help.expressions.paragraph4 + - timelion.help.expressions.functionReferenceLinkText" + i18n-description="Part of composite text + timelion.help.expressions.paragraph4 + + timelion.help.expressions.functionReferenceLinkText" > .

@@ -556,19 +556,19 @@

diff --git a/src/core_plugins/vega/public/vega_type.js b/src/core_plugins/vega/public/vega_type.js index e96fba000b4c08c..f17852cab9045ec 100644 --- a/src/core_plugins/vega/public/vega_type.js +++ b/src/core_plugins/vega/public/vega_type.js @@ -65,7 +65,7 @@ VisTypesRegistryProvider.register((Private) => { showQueryBar: true, showFilterBar: true, }, - stage: 'lab', + stage: 'experimental', feedbackMessage: defaultFeedbackMessage, }); }); diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service index 4a0bf42cef8d371..f079d44a1f5284e 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service @@ -1,6 +1,8 @@ [Unit] Description=Kibana - +StartLimitIntervalSec=30 +StartLimitBurst=3 + [Service] Type=simple User=kibana diff --git a/src/dev/ci_setup/git_setup.sh b/src/dev/ci_setup/git_setup.sh index 8836d80f4327c10..360173a840a6e93 100755 --- a/src/dev/ci_setup/git_setup.sh +++ b/src/dev/ci_setup/git_setup.sh @@ -52,6 +52,9 @@ function checkout_sibling { } function pick_clone_target { + echo "To develop Kibana features against a specific branch of ${project} and being able to" + echo "test that feature also on CI, the CI is trying to find branches on ${project} with the same name as" + echo "the Kibana branch (first on your fork and then upstream) before building from master." echo "picking which branch of ${project} to clone:" if [[ -n "$PR_AUTHOR" && -n "$PR_SOURCE_BRANCH" ]]; then cloneAuthor="$PR_AUTHOR" diff --git a/src/dev/ci_setup/load_bootstrap_cache.sh b/src/dev/ci_setup/load_bootstrap_cache.sh new file mode 100755 index 000000000000000..cbbd46a7ff65285 --- /dev/null +++ b/src/dev/ci_setup/load_bootstrap_cache.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +bootstrapCache="$HOME/.kibana/bootstrap_cache/master.tar" + +if [ -f "$bootstrapCache" ]; then + echo "extracting bootstrap_cache from $bootstrapCache"; + tar -xf "$bootstrapCache"; +else + echo "bootstrap_cache missing"; + exit 1; +fi diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 5a8738018b68106..71f8b10513e5ee3 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -84,6 +84,11 @@ hash -r yarnVersion="$(node -e "console.log(String(require('./package.json').engines.yarn || '').replace(/^[^\d]+/,''))")" npm install -g yarn@^${yarnVersion} +### +### setup yarn offline cache +### +yarn config set yarn-offline-mirror "$cacheDir/yarn-offline-cache" + ### ### "install" yarn into this shell ### @@ -95,7 +100,7 @@ hash -r ### install dependencies ### echo " -- installing node.js dependencies" -yarn kbn bootstrap +yarn kbn bootstrap --prefer-offline ### ### verify no git modifications diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 275e051ff8ca59a..9860e5310930441 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -93,7 +93,7 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) { const entries = await globAsync('*.{js,jsx,pug,ts,tsx,html,hbs,handlebars}', { cwd: inputPath, matchBase: true, - ignore: ['**/node_modules/**', '**/__tests__/**', '**/*.test.{js,jsx,ts,tsx}'], + ignore: ['**/node_modules/**', '**/__tests__/**', '**/*.test.{js,jsx,ts,tsx}', '**/*.d.ts'], }); const { htmlEntries, codeEntries, pugEntries, hbsEntries } = entries.reduce( diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 9f8f042b2ddb708..9d05e1dabf59c2e 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -49,7 +49,7 @@ export default class JestJUnitReporter { * @return {undefined} */ onRunComplete(contexts, results) { - if (!process.env.CI) { + if (!process.env.CI || !results.testResults.length) { return; } diff --git a/src/dev/mocha/junit_report_generation.js b/src/dev/mocha/junit_report_generation.js index e5c7aec947b8fb6..74b271e82f52eed 100644 --- a/src/dev/mocha/junit_report_generation.js +++ b/src/dev/mocha/junit_report_generation.js @@ -27,6 +27,8 @@ import xmlBuilder from 'xmlbuilder'; import { getSnapshotOfRunnableLogs } from './log_cache'; import { escapeCdata } from '../xml'; +const dateNow = Date.now.bind(Date); + export function setupJUnitReportGeneration(runner, options = {}) { const { reportName = 'Unnamed Mocha Tests', @@ -47,11 +49,11 @@ export function setupJUnitReportGeneration(runner, options = {}) { ); const setStartTime = (node) => { - node.startTime = Date.now(); + node.startTime = dateNow(); }; const setEndTime = node => { - node.endTime = Date.now(); + node.endTime = dateNow(); }; const getFullTitle = node => { @@ -85,6 +87,9 @@ export function setupJUnitReportGeneration(runner, options = {}) { runner.on('end', () => { // crawl the test graph to collect all defined tests const allTests = findAllTests(runner.suite); + if (!allTests.length) { + return; + } // filter out just the failures const failures = results.filter(result => result.failed); diff --git a/src/dev/register_git_hook/index.js b/src/dev/register_git_hook/index.js new file mode 100644 index 000000000000000..6089256423ff604 --- /dev/null +++ b/src/dev/register_git_hook/index.js @@ -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 { registerPrecommitGitHook } from './register_git_hook'; diff --git a/src/dev/register_git_hook/register_git_hook.js b/src/dev/register_git_hook/register_git_hook.js new file mode 100644 index 000000000000000..670a3cb2b3aeaf3 --- /dev/null +++ b/src/dev/register_git_hook/register_git_hook.js @@ -0,0 +1,100 @@ +/* + * 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 chalk from 'chalk'; +import { chmod, unlink, writeFile } from 'fs'; +import dedent from 'dedent'; +import { resolve } from 'path'; +import { promisify } from 'util'; +import SimpleGit from 'simple-git'; +import { REPO_ROOT } from '../constants'; + +const simpleGit = new SimpleGit(REPO_ROOT); + +const chmodAsync = promisify(chmod); +const gitRevParseAsync = promisify(simpleGit.revparse.bind(simpleGit)); +const unlinkAsync = promisify(unlink); +const writeFileAsync = promisify(writeFile); + +async function getPrecommitGitHookScriptPath(rootPath) { + // Retrieves the correct location for the .git dir for + // every git setup (including git worktree) + const gitDirPath = (await gitRevParseAsync(['--git-dir'])).trim(); + + return resolve(rootPath, gitDirPath, 'hooks/pre-commit'); +} + +function getKbnPrecommitGitHookScript(rootPath) { + return dedent(` + #!/usr/bin/env bash + # + # ** THIS IS AN AUTO-GENERATED FILE ** + # ** PLEASE DO NOT CHANGE IT MANUALLY ** + # + # GENERATED BY ${__dirname} + # IF YOU WANNA CHANGE SOMETHING INTO THIS SCRIPT + # PLEASE RE-RUN 'yarn kbn bootstrap' or 'node scripts/register_git_hook' IN THE ROOT + # OF THE CURRENT PROJECT ${rootPath} + + set -euo pipefail + + # Export Git hook params + export GIT_PARAMS="$*" + + has_node() { + command -v node >/dev/null 2>&1 + } + + # Check if we have node js bin in path + has_node || { echo "Can't found node bin in the PATH. Please update the PATH to proceed"; exit 1; } + + npm run --silent precommit || { echo "Pre-commit hook failed (add --no-verify to bypass)"; exit 1; } + + exit 0 + `); +} + +export async function registerPrecommitGitHook(log) { + log.write( + chalk.bold( + `Registering Kibana pre-commit git hook...\n` + ) + ); + + try { + await writeGitHook( + await getPrecommitGitHookScriptPath(REPO_ROOT), + getKbnPrecommitGitHookScript(REPO_ROOT) + ); + } catch (e) { + log.write(`${chalk.red('fail')} Kibana pre-commit git hook was not installed as an error occur.\n`); + throw e; + } + + log.write(`${chalk.green('success')} Kibana pre-commit git hook was installed successfully.\n`); +} + +async function writeGitHook(gitHookScriptPath, kbnHookScriptSource) { + try { + await unlinkAsync(gitHookScriptPath); + } catch (e) { /* no-op */ } + + await writeFileAsync(gitHookScriptPath, kbnHookScriptSource); + await chmodAsync(gitHookScriptPath, 0o755); +} diff --git a/src/dev/run/index.d.ts b/src/dev/run/index.d.ts index cec8dd88c401272..8d809f0e94d3b4b 100644 --- a/src/dev/run/index.d.ts +++ b/src/dev/run/index.d.ts @@ -17,4 +17,4 @@ * under the License. */ -export function createFailError(msg: string, exitCode: number): Error; +export function createFailError(msg: string, exitCode?: number): Error; diff --git a/src/dev/run_register_git_hook.js b/src/dev/run_register_git_hook.js new file mode 100644 index 000000000000000..ffa6552f4ff1f76 --- /dev/null +++ b/src/dev/run_register_git_hook.js @@ -0,0 +1,29 @@ +/* + * 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 { run, createFailError } from './run'; +import { registerPrecommitGitHook } from './register_git_hook'; + +run(async ({ log }) => { + try { + await registerPrecommitGitHook(log); + } catch (error) { + throw createFailError(error); + } +}); diff --git a/src/fixtures/vislib/_vis_fixture.js b/src/fixtures/vislib/_vis_fixture.js index 056e0a2141a62f5..c61d21a305d24db 100644 --- a/src/fixtures/vislib/_vis_fixture.js +++ b/src/fixtures/vislib/_vis_fixture.js @@ -41,7 +41,7 @@ const visHeight = $visCanvas.height(); $visCanvas.new = function () { count += 1; if (count > 1) $visCanvas.height(visHeight * count); - return $('
').addClass('visualize-chart').appendTo($visCanvas); + return $('
').addClass('visChart').appendTo($visCanvas); }; afterEach(function () { diff --git a/src/functional_test_runner/cli.js b/src/functional_test_runner/cli.js index 6998a1b8bfb7a23..9414dcfe1cfd937 100644 --- a/src/functional_test_runner/cli.js +++ b/src/functional_test_runner/cli.js @@ -48,6 +48,7 @@ cmd .option('--exclude [file]', 'Path to a test file that should not be loaded', collectExcludePaths(), []) .option('--include-tag [tag]', 'A tag to be included, pass multiple times for multiple tags', collectIncludeTags(), []) .option('--exclude-tag [tag]', 'A tag to be excluded, pass multiple times for multiple tags', collectExcludeTags(), []) + .option('--test-stats', 'Print the number of tests (included and excluded) to STDERR', false) .option('--verbose', 'Log everything', false) .option('--quiet', 'Only log errors', false) .option('--silent', 'Log nothing', false) @@ -86,8 +87,16 @@ const functionalTestRunner = createFunctionalTestRunner({ async function run() { try { - const failureCount = await functionalTestRunner.run(); - process.exitCode = failureCount ? 1 : 0; + if (cmd.testStats) { + process.stderr.write(JSON.stringify( + await functionalTestRunner.getTestStats(), + null, + 2 + ) + '\n'); + } else { + const failureCount = await functionalTestRunner.run(); + process.exitCode = failureCount ? 1 : 0; + } } catch (err) { await teardown(err); } finally { diff --git a/src/functional_test_runner/functional_test_runner.js b/src/functional_test_runner/functional_test_runner.js index 65d347fbee45091..9356412ba0738a6 100644 --- a/src/functional_test_runner/functional_test_runner.js +++ b/src/functional_test_runner/functional_test_runner.js @@ -20,7 +20,8 @@ import { createLifecycle, readConfigFile, - createProviderCollection, + ProviderCollection, + readProviderSpec, setupMocha, runTests, } from './lib'; @@ -36,8 +37,65 @@ export function createFunctionalTestRunner({ log, configFile, configOverrides }) log.verbose('ending %j lifecycle phase', name); }); + class FunctionalTestRunner { async run() { + return await this._run(async (config, coreProviders) => { + const providers = new ProviderCollection(log, [ + ...coreProviders, + ...readProviderSpec('Service', config.get('services')), + ...readProviderSpec('PageObject', config.get('pageObjects')) + ]); + + await providers.loadAll(); + + const mocha = await setupMocha(lifecycle, log, config, providers); + await lifecycle.trigger('beforeTests'); + log.info('Starting tests'); + + return await runTests(lifecycle, log, mocha); + }); + } + + async getTestStats() { + return await this._run(async (config, coreProviders) => { + // replace the function of custom service providers so that they return + // promise-like objects which never resolve, essentially disabling them + // allowing us to load the test files and populate the mocha suites + const stubProvider = provider => ( + coreProviders.includes(provider) + ? provider + : { + ...provider, + fn: () => ({ + then: () => {} + }) + } + ); + + const providers = new ProviderCollection(log, [ + ...coreProviders, + ...readProviderSpec('Service', config.get('services')), + ...readProviderSpec('PageObject', config.get('pageObjects')) + ].map(stubProvider)); + + const mocha = await setupMocha(lifecycle, log, config, providers); + + const countTests = suite => ( + suite.suites.reduce( + (sum, suite) => sum + countTests(suite), + suite.tests.length + ) + ); + + return { + testCount: countTests(mocha.suite), + excludedTests: mocha.excludedTests.map(t => t.fullTitle()) + }; + }); + } + + async _run(handler) { let runErrorOccurred = false; try { @@ -49,14 +107,14 @@ export function createFunctionalTestRunner({ log, configFile, configOverrides }) return; } - const providers = createProviderCollection(lifecycle, log, config); - await providers.loadAll(); - - const mocha = await setupMocha(lifecycle, log, config, providers); - await lifecycle.trigger('beforeTests'); - log.info('Starting tests'); - return await runTests(lifecycle, log, mocha); + // base level services that functional_test_runner exposes + const coreProviders = readProviderSpec('Service', { + lifecycle: () => lifecycle, + log: () => log, + config: () => config, + }); + return await handler(config, coreProviders); } catch (runError) { runErrorOccurred = true; throw runError; diff --git a/src/functional_test_runner/lib/config/read_config_file.js b/src/functional_test_runner/lib/config/read_config_file.js index bd3cddcf8e25a1d..e733d2b989ff3e9 100644 --- a/src/functional_test_runner/lib/config/read_config_file.js +++ b/src/functional_test_runner/lib/config/read_config_file.js @@ -22,18 +22,17 @@ import { defaultsDeep } from 'lodash'; import { Config } from './config'; import { transformDeprecations } from './transform_deprecations'; -async function getSettingsFromFile(log, path, settingOverrides) { - log.debug('Loading config file from %j', path); +const cache = new WeakMap(); +async function getSettingsFromFile(log, path, settingOverrides) { const configModule = require(path); const configProvider = configModule.__esModule ? configModule.default : configModule; - const settingsWithDefaults = defaultsDeep( - {}, - settingOverrides, - await configProvider({ + if (!cache.has(configProvider)) { + log.debug('Loading config file from %j', path); + cache.set(configProvider, configProvider({ log, async readConfigFile(...args) { return new Config({ @@ -42,7 +41,13 @@ async function getSettingsFromFile(log, path, settingOverrides) { path, }); } - }) + })); + } + + const settingsWithDefaults = defaultsDeep( + {}, + settingOverrides, + await cache.get(configProvider) ); const logDeprecation = (...args) => log.error(...args); diff --git a/src/functional_test_runner/lib/create_provider_collection.js b/src/functional_test_runner/lib/create_provider_collection.js deleted file mode 100644 index e91f473290ec35c..000000000000000 --- a/src/functional_test_runner/lib/create_provider_collection.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - ProviderCollection, - readProviderSpec -} from './providers'; - -/** - * Create a ProviderCollection that includes the Service - * providers and PageObject providers from config, as well - * providers for the default services, lifecycle, log, and - * config - * - * @param {Lifecycle} lifecycle - * @param {ToolingLog} log - * @param {Config} config [description] - * @return {ProviderCollection} - */ -export function createProviderCollection(lifecycle, log, config) { - return new ProviderCollection(log, [ - ...readProviderSpec('Service', { - // base level services that functional_test_runner exposes - lifecycle: () => lifecycle, - log: () => log, - config: () => config, - - ...config.get('services'), - }), - ...readProviderSpec('PageObject', { - ...config.get('pageObjects') - }) - ]); -} diff --git a/src/functional_test_runner/lib/index.js b/src/functional_test_runner/lib/index.js index 1a933447bb0fb13..e1f93d8ac627c4c 100644 --- a/src/functional_test_runner/lib/index.js +++ b/src/functional_test_runner/lib/index.js @@ -19,5 +19,5 @@ export { createLifecycle } from './lifecycle'; export { readConfigFile } from './config'; -export { createProviderCollection } from './create_provider_collection'; export { setupMocha, runTests } from './mocha'; +export { readProviderSpec, ProviderCollection } from './providers'; diff --git a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js b/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js index fd15f936c762552..597be9bab32a207 100644 --- a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js +++ b/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js @@ -28,6 +28,14 @@ * @param options.exclude an array of tags that will be used to exclude suites from the run */ export function filterSuitesByTags({ log, mocha, include, exclude }) { + mocha.excludedTests = []; + // collect all the tests from some suite, including it's children + const collectTests = (suite) => + suite.suites.reduce( + (acc, s) => acc.concat(collectTests(s)), + suite.tests + ); + // if include tags were provided, filter the tree once to // only include branches that are included at some point if (include.length) { @@ -47,14 +55,18 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { continue; } + // this suite has an included child but is not included // itself, so strip out its tests and recurse to filter // out child suites which are not included if (isChildIncluded(child)) { + mocha.excludedTests = mocha.excludedTests.concat(child.tests); child.tests = []; parentSuite.suites.push(child); recurse(child); continue; + } else { + mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); } } }(mocha.suite)); @@ -78,6 +90,8 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { if (isNotExcluded(child)) { parentSuite.suites.push(child); recurse(child); + } else { + mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); } } }(mocha.suite)); diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index b0fe947f569205d..1bf29fbac1d9d72 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -150,8 +150,9 @@ export default class BaseOptimizer { }, { test, - include: /[\/\\]node_modules[\/\\]x-pack[\/\\]/, - exclude: /[\/\\]node_modules[\/\\]x-pack[\/\\](.+?[\/\\])*node_modules[\/\\]/, + include: /[\/\\]node_modules[\/\\](x-pack|@kbn[\/\\]interpreter)[\/\\]/, + exclude: /[\/\\]node_modules[\/\\](x-pack|@kbn[\/\\]interpreter)[\/\\]node_modules[\/\\]/, + } ]; }; diff --git a/src/server/status/routes/page/register_status.js b/src/server/status/routes/page/register_status.js index a99162ab4f47b33..b79a3f442c68f41 100644 --- a/src/server/status/routes/page/register_status.js +++ b/src/server/status/routes/page/register_status.js @@ -20,14 +20,21 @@ import { wrapAuthConfig } from '../../wrap_auth_config'; export function registerStatusPage(kbnServer, server, config) { - const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); + const allowAnonymous = config.get('status.allowAnonymous'); + const wrapAuth = wrapAuthConfig(allowAnonymous); server.decorate('toolkit', 'renderStatusPage', async function () { const app = server.getHiddenUiAppById('status_page'); const h = this; - const response = app - ? await h.renderApp(app) - : h.response(kbnServer.status.toString()); + + let response; + // An unauthenticated (anonymous) user may not have access to the customized configuration. + // For this scenario, render with the default config. + if (app) { + response = allowAnonymous ? await h.renderAppWithDefaultConfig(app) : await h.renderApp(app); + } else { + h.response(kbnServer.status.toString()); + } if (response) { return response.code(kbnServer.status.isGreen() ? 200 : 503); diff --git a/src/setup_node_env/babel_register/register.js b/src/setup_node_env/babel_register/register.js index 2d909636a02a831..17dd7f0705df11e 100644 --- a/src/setup_node_env/babel_register/register.js +++ b/src/setup_node_env/babel_register/register.js @@ -38,8 +38,12 @@ var ignore = [ // https://github.com/elastic/kibana/issues/14800#issuecomment-366130268 // ignore paths matching `/node_modules/{a}/{b}`, unless `a` - // is `x-pack` and `b` is not `node_modules` - /\/node_modules\/(?!x-pack\/(?!node_modules)([^\/]+))([^\/]+\/[^\/]+)/ + // is `x-pack` or `@kbn/interpreter` and `b` is not `node_modules` + /\/node_modules\/(?!(x-pack\/|@kbn\/interpreter\/)(?!node_modules)([^\/]+))([^\/]+\/[^\/]+)/, + + // ignore paths matching `/canvas/canvas_plugin/{a}/{b}` unless + // `a` is `functions` and `b` is `server` + /\/canvas\/canvas_plugin\/(?!functions\/server)([^\/]+\/[^\/]+)/, ]; if (global.__BUILT_WITH_BABEL__) { diff --git a/src/ui/public/_index.scss b/src/ui/public/_index.scss new file mode 100644 index 000000000000000..828c9b6860d80f9 --- /dev/null +++ b/src/ui/public/_index.scss @@ -0,0 +1,5 @@ +@import './query_bar/index'; +// Can't import vis folder here because of cascading issues, it's imported in core_plugins/kibana +// @import './vis/index'; +@import './vislib/index'; +@import './visualize/index'; diff --git a/src/ui/public/agg_response/hierarchical/_tooltip.html b/src/ui/public/agg_response/hierarchical/_tooltip.html index 6688d9270910f8a..de59cf871d46f54 100644 --- a/src/ui/public/agg_response/hierarchical/_tooltip.html +++ b/src/ui/public/agg_response/hierarchical/_tooltip.html @@ -1,15 +1,15 @@ - +
- + - + - + diff --git a/src/ui/public/agg_response/point_series/_tooltip.html b/src/ui/public/agg_response/point_series/_tooltip.html index 2384fab317f2b19..d6054594ec776f0 100644 --- a/src/ui/public/agg_response/point_series/_tooltip.html +++ b/src/ui/public/agg_response/point_series/_tooltip.html @@ -1,8 +1,8 @@
field value {{metricCol.label}}
{{row.field}}{{row.bucket}}{{row.bucket}} {{row.metric}}
- - + diff --git a/src/ui/public/agg_types/buckets/date_histogram.js b/src/ui/public/agg_types/buckets/date_histogram.js index 78c926e9cdfcfa8..d430c87bdd2fdde 100644 --- a/src/ui/public/agg_types/buckets/date_histogram.js +++ b/src/ui/public/agg_types/buckets/date_histogram.js @@ -17,10 +17,9 @@ * under the License. */ -import { jstz as tzDetect } from 'jstimezonedetect'; import _ from 'lodash'; import chrome from '../../chrome'; -import moment from 'moment'; +import moment from 'moment-timezone'; import '../../filters/field_type'; import '../../validate_date_interval'; import { BucketAggType } from './_bucket_agg_type'; @@ -32,7 +31,7 @@ import { timefilter } from '../../timefilter'; import dropPartialTemplate from '../controls/drop_partials.html'; const config = chrome.getUiSettingsClient(); -const detectedTimezone = tzDetect.determine().name(); +const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); function getInterval(agg) { diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index 19fcfa410f2482f..c9ed0153fe9754c 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -67,6 +67,7 @@ export function initAngularApi(chrome, internals) { $locationProvider.hashPrefix(''); }) .run(internals.capture$httpLoadingCount) + .run(internals.$setupBreadcrumbsAutoClear) .run(($location, $rootScope, Private, config) => { chrome.getFirstPathSegment = () => { return $location.path().split('/')[1]; diff --git a/src/ui/public/chrome/api/breadcrumbs.ts b/src/ui/public/chrome/api/breadcrumbs.ts new file mode 100644 index 000000000000000..83c29a2bcb7720a --- /dev/null +++ b/src/ui/public/chrome/api/breadcrumbs.ts @@ -0,0 +1,75 @@ +/* + * 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. + */ + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { Breadcrumb, ChromeStartContract } from '../../../../core/public/chrome'; +export { Breadcrumb }; + +let newPlatformChrome: ChromeStartContract; +export function __newPlatformInit__(instance: ChromeStartContract) { + if (newPlatformChrome) { + throw new Error('ui/chrome/api/breadcrumbs is already initialized'); + } + + newPlatformChrome = instance; +} + +export function initBreadcrumbsApi( + chrome: { [key: string]: any }, + internals: { [key: string]: any } +) { + // A flag used to determine if we should automatically + // clear the breadcrumbs between angular route changes. + let shouldClear = false; + + // reset shouldClear any time the breadcrumbs change, even + // if it was done directly through the new platform + newPlatformChrome.getBreadcrumbs$().subscribe({ + next() { + shouldClear = false; + }, + }); + + chrome.breadcrumbs = { + get$() { + return newPlatformChrome.getBreadcrumbs$(); + }, + + set(newBreadcrumbs: Breadcrumb[]) { + newPlatformChrome.setBreadcrumbs(newBreadcrumbs); + }, + }; + + // define internal angular run function that will be called when angular + // bootstraps and lets us integrate with the angular router so that we can + // automatically clear the breadcrumbs if we switch to a Kibana app that + // does not use breadcrumbs correctly + internals.$setupBreadcrumbsAutoClear = ($rootScope: any) => { + $rootScope.$on('$routeChangeStart', () => { + shouldClear = true; + }); + + $rootScope.$on('$routeChangeSuccess', () => { + if (shouldClear) { + newPlatformChrome.setBreadcrumbs([]); + } + }); + }; +} diff --git a/src/ui/public/chrome/chrome.js b/src/ui/public/chrome/chrome.js index ffd3e5da2594807..7eba9510768036f 100644 --- a/src/ui/public/chrome/chrome.js +++ b/src/ui/public/chrome/chrome.js @@ -35,6 +35,7 @@ import { initAngularApi } from './api/angular'; import appsApi from './api/apps'; import { initChromeControlsApi } from './api/controls'; import { initChromeNavApi } from './api/nav'; +import { initBreadcrumbsApi } from './api/breadcrumbs'; import templateApi from './api/template'; import { initChromeThemeApi } from './api/theme'; import { initChromeXsrfApi } from './api/xsrf'; @@ -67,6 +68,7 @@ initChromeXsrfApi(chrome, internals); initChromeBasePathApi(chrome); initChromeInjectedVarsApi(chrome); initChromeNavApi(chrome, internals); +initBreadcrumbsApi(chrome, internals); initLoadingCountApi(chrome, internals); initAngularApi(chrome, internals); initChromeControlsApi(chrome); diff --git a/src/ui/public/chrome/directives/header_global_nav/components/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/ui/public/chrome/directives/header_global_nav/components/__snapshots__/header_breadcrumbs.test.tsx.snap index 74243a506a0fd9a..4b97c565f3d4b50 100644 --- a/src/ui/public/chrome/directives/header_global_nav/components/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/ui/public/chrome/directives/header_global_nav/components/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`HeaderBreadcrumbs renders updates to the breadcrumbs observable 1`] = ` +exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = ` `; -exports[`HeaderBreadcrumbs renders updates to the breadcrumbs observable 2`] = ` +exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 2`] = ` Array [ ; + breadcrumbs$: Rx.Observable; homeHref: string; isVisible: boolean; navLinks: NavLink[]; @@ -66,7 +67,7 @@ class HeaderUI extends Component { } public render() { - const { appTitle, breadcrumbs, isVisible, navControls, navLinks } = this.props; + const { appTitle, breadcrumbs$, isVisible, navControls, navLinks } = this.props; if (!isVisible) { return null; @@ -82,7 +83,7 @@ class HeaderUI extends Component { - + diff --git a/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.test.tsx b/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.test.tsx index 73c3724b1a95a41..dabf36f91317e6c 100644 --- a/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.test.tsx +++ b/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.test.tsx @@ -19,23 +19,25 @@ import { mount } from 'enzyme'; import React from 'react'; -import { breadcrumbs, set } from '../../../services/breadcrumb_state'; +import * as Rx from 'rxjs'; +import { Breadcrumb } from '../../../../../../core/public/chrome'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; describe('HeaderBreadcrumbs', () => { - it('renders updates to the breadcrumbs observable', () => { - const wrapper = mount(); + it('renders updates to the breadcrumbs$ observable', () => { + const breadcrumbs$ = new Rx.Subject(); + const wrapper = mount(); - set([{ text: 'First' }]); + breadcrumbs$.next([{ text: 'First' }]); // Unfortunately, enzyme won't update the wrapper until we call update. wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - set([{ text: 'First' }, { text: 'Second' }]); + breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }]); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - set([]); + breadcrumbs$.next([]); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); }); diff --git a/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.tsx b/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.tsx index ff880ccbd9f67a1..828de8bf7d3141e 100644 --- a/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.tsx +++ b/src/ui/public/chrome/directives/header_global_nav/components/header_breadcrumbs.tsx @@ -18,18 +18,18 @@ */ import React, { Component } from 'react'; -import { Subscribable, Unsubscribable } from 'rxjs'; +import * as Rx from 'rxjs'; import { // @ts-ignore EuiHeaderBreadcrumbs, } from '@elastic/eui'; -import { Breadcrumb } from '../'; +import { Breadcrumb } from '../../../../../../core/public/chrome'; interface Props { appTitle?: string; - breadcrumbs: Subscribable; + breadcrumbs$: Rx.Observable; } interface State { @@ -37,7 +37,7 @@ interface State { } export class HeaderBreadcrumbs extends Component { - private unsubscribable?: Unsubscribable; + private subscription?: Rx.Subscription; constructor(props: Props) { super(props); @@ -50,7 +50,7 @@ export class HeaderBreadcrumbs extends Component { } public componentDidUpdate(prevProps: Props) { - if (prevProps.breadcrumbs === this.props.breadcrumbs) { + if (prevProps.breadcrumbs$ === this.props.breadcrumbs$) { return; } @@ -73,15 +73,17 @@ export class HeaderBreadcrumbs extends Component { } private subscribe() { - this.unsubscribable = this.props.breadcrumbs.subscribe(breadcrumbs => { - this.setState({ breadcrumbs }); + this.subscription = this.props.breadcrumbs$.subscribe(breadcrumbs => { + this.setState({ + breadcrumbs, + }); }); } private unsubscribe() { - if (this.unsubscribable) { - this.unsubscribable.unsubscribe(); - delete this.unsubscribable; + if (this.subscription) { + this.subscription.unsubscribe(); + delete this.subscription; } } } diff --git a/src/ui/public/chrome/directives/header_global_nav/header_global_nav.js b/src/ui/public/chrome/directives/header_global_nav/header_global_nav.js index 1d6048b3f6b7c3d..10773e22894fcdf 100644 --- a/src/ui/public/chrome/directives/header_global_nav/header_global_nav.js +++ b/src/ui/public/chrome/directives/header_global_nav/header_global_nav.js @@ -22,7 +22,6 @@ import { uiModules } from '../../../modules'; import { Header } from './components/header'; import './header_global_nav.less'; import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls'; -import { breadcrumbs } from '../../services/breadcrumb_state'; import { injectI18nProvider } from '@kbn/i18n/react'; const module = uiModules.get('kibana'); @@ -40,7 +39,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private) => { {}, // angular injected React props { - breadcrumbs, + breadcrumbs$: chrome.breadcrumbs.get$(), navLinks, navControls, homeHref diff --git a/src/ui/public/chrome/directives/header_global_nav/index.ts b/src/ui/public/chrome/directives/header_global_nav/index.ts index 9b6b7521c2c4df0..43e0d04916b7ada 100644 --- a/src/ui/public/chrome/directives/header_global_nav/index.ts +++ b/src/ui/public/chrome/directives/header_global_nav/index.ts @@ -38,8 +38,3 @@ export interface NavLink { id: string; euiIconType: IconType; } - -export interface Breadcrumb { - text: string; - href?: string; -} diff --git a/src/ui/public/chrome/index.d.ts b/src/ui/public/chrome/index.d.ts index 533157cecf5ef1d..4afa5a5eb453e36 100644 --- a/src/ui/public/chrome/index.d.ts +++ b/src/ui/public/chrome/index.d.ts @@ -17,6 +17,8 @@ * under the License. */ +import { Brand } from '../../../core/public/chrome'; + interface IInjector { get(injectable: string): T; } @@ -30,6 +32,12 @@ declare class Chrome { public getUiSettingsClient(): any; public setVisible(visible: boolean): any; public getInjected(key: string, defaultValue?: any): any; + public setRootController(name: string, Controller: any): any; + public setBrand(brand: Brand): this; + public getBrand(key: keyof Brand): Brand[keyof Brand]; + public addApplicationClass(classNames: string | string[]): this; + public removeApplicationClass(classNames: string | string[]): this; + public getApplicationClasses(): string; } declare const chrome: Chrome; diff --git a/src/ui/public/chrome/services/breadcrumb_state.ts b/src/ui/public/chrome/services/breadcrumb_state.ts deleted file mode 100644 index 020c9f133396871..000000000000000 --- a/src/ui/public/chrome/services/breadcrumb_state.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Subject, Subscribable } from 'rxjs'; -// @ts-ignore -import { uiModules } from '../../modules'; -import { Breadcrumb } from '../directives/header_global_nav'; - -// A flag used to keep track of clearing between route changes. -let shouldClear = false; - -// Subject used by Header component to subscribe to breadcrumbs changes. -// This is not exposed publicly. -const breadcrumbsSubject = new Subject(); - -/** - * A rxjs subscribable that can be used to subscribe to breadcrumb updates. - */ -export const breadcrumbs: Subscribable = breadcrumbsSubject; - -/** - * Should be called by plugins to set breadcrumbs in the header navigation. - * - * @param breadcrumbs: Array where Breadcrumb has shape - * { text: '', href?: '' } - */ -export const set = (newBreadcrumbs: Breadcrumb[]) => { - breadcrumbsSubject.next(newBreadcrumbs); - - // If a plugin called set, don't clear on route change. - shouldClear = false; -}; - -uiModules.get('kibana').service('breadcrumbState', ($rootScope: any) => { - // When a route change happens we want to clear the breadcrumbs ONLY if - // the new route does not set any breadcrumbs. Deferring the clearing until - // the route finishes changing helps avoiding the breadcrumbs from 'flickering'. - $rootScope.$on('$routeChangeStart', () => (shouldClear = true)); - $rootScope.$on('$routeChangeSuccess', () => { - if (shouldClear) { - set([]); - } - }); - - return { set }; -}); diff --git a/src/ui/public/chrome/services/index.js b/src/ui/public/chrome/services/index.js index 3c2f2fb71d202f0..3b3967f51b2ffa7 100644 --- a/src/ui/public/chrome/services/index.js +++ b/src/ui/public/chrome/services/index.js @@ -18,4 +18,3 @@ */ import './global_nav_state'; -import './breadcrumb_state'; diff --git a/src/ui/public/directives/saved_object_finder.js b/src/ui/public/directives/saved_object_finder.js index ad9905047f0b627..0b107dc085f2d46 100644 --- a/src/ui/public/directives/saved_object_finder.js +++ b/src/ui/public/directives/saved_object_finder.js @@ -286,7 +286,7 @@ module.directive('savedObjectFinder', function ($location, $injector, kbnUrl, Pr self.service.find(filter) .then(function (hits) { - hits.hits = hits.hits.filter((hit) => (isLabsEnabled || _.get(hit, 'type.stage') !== 'lab')); + hits.hits = hits.hits.filter((hit) => (isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental')); hits.total = hits.hits.length; // ensure that we don't display old results diff --git a/src/ui/public/filter_bar/lib/__tests__/change_time_filter.test.js b/src/ui/public/filter_bar/lib/__tests__/change_time_filter.test.js index eab08903ec029c1..6f02c06aea5b13a 100644 --- a/src/ui/public/filter_bar/lib/__tests__/change_time_filter.test.js +++ b/src/ui/public/filter_bar/lib/__tests__/change_time_filter.test.js @@ -36,31 +36,30 @@ jest.mock('ui/chrome', }, }), { virtual: true }); -import moment from 'moment'; import expect from 'expect.js'; import { changeTimeFilter } from '../change_time_filter'; import { timefilter } from 'ui/timefilter'; describe('changeTimeFilter()', function () { + const gt = 1388559600000; + const lt = 1388646000000; test('should change the timefilter to match the range gt/lt', function () { - const filter = { range: { '@timestamp': { gt: 1388559600000, lt: 1388646000000 } } }; + const filter = { range: { '@timestamp': { gt, lt } } }; changeTimeFilter(filter); - expect(timefilter.getTime().mode).to.be('absolute'); - expect(moment.isMoment(timefilter.getTime().to)).to.be(true); - expect(timefilter.getTime().to.isSame('2014-01-02')); - expect(moment.isMoment(timefilter.getTime().from)).to.be(true); - expect(timefilter.getTime().from.isSame('2014-01-01')); + const { mode, to, from } = timefilter.getTime(); + expect(mode).to.be('absolute'); + expect(to).to.be(new Date(lt).toISOString()); + expect(from).to.be(new Date(gt).toISOString()); }); test('should change the timefilter to match the range gte/lte', function () { - const filter = { range: { '@timestamp': { gte: 1388559600000, lte: 1388646000000 } } }; + const filter = { range: { '@timestamp': { gte: gt, lte: lt } } }; changeTimeFilter(filter); - expect(timefilter.getTime().mode).to.be('absolute'); - expect(moment.isMoment(timefilter.getTime().to)).to.be(true); - expect(timefilter.getTime().to.isSame('2014-01-02')); - expect(moment.isMoment(timefilter.getTime().from)).to.be(true); - expect(timefilter.getTime().from.isSame('2014-01-01')); + const { mode, to, from } = timefilter.getTime(); + expect(mode).to.be('absolute'); + expect(to).to.be(new Date(lt).toISOString()); + expect(from).to.be(new Date(gt).toISOString()); }); }); diff --git a/src/ui/public/index_patterns/_get_computed_fields.js b/src/ui/public/index_patterns/_get_computed_fields.js index 527ca74821eb3de..422165b89032457 100644 --- a/src/ui/public/index_patterns/_get_computed_fields.js +++ b/src/ui/public/index_patterns/_get_computed_fields.js @@ -18,13 +18,15 @@ */ import _ from 'lodash'; -// Takes a hit, merges it with any stored/scripted fields, and with the metaFields -// returns a flattened version + export function getComputedFields() { const self = this; const scriptFields = {}; let docvalueFields = []; + // Date value returned in "_source" could be in any number of formats + // Use a docvalue for each date field to ensure standardized formats when working with date fields + // indexPattern.flattenHit will override "_source" values when the same field is also defined in "fields" docvalueFields = _.reject(self.fields.byType.date, 'scripted') .map((dateField) => { return { diff --git a/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js index d0674cc68d2d547..0c6999388749733 100644 --- a/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js +++ b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.js @@ -20,6 +20,7 @@ import breadCrumbsTemplate from './bread_crumbs.html'; import { uiModules } from '../../modules'; import uiRouter from '../../routes'; +import chrome from '../../chrome'; const module = uiModules.get('kibana'); @@ -46,7 +47,7 @@ module.directive('breadCrumbs', function () { useLinks: '=' }, template: breadCrumbsTemplate, - controller: function ($scope, config, breadcrumbState) { + controller: function ($scope, config) { config.watch('k7design', (val) => $scope.showPluginBreadcrumbs = !val); function omitPagesFilter(crumb) { @@ -78,7 +79,7 @@ module.directive('breadCrumbs', function () { newBreadcrumbs.push({ text: $scope.pageTitle }); } - breadcrumbState.set(newBreadcrumbs); + chrome.breadcrumbs.set(newBreadcrumbs); }); } }; diff --git a/src/ui/public/saved_objects/components/saved_object_finder.js b/src/ui/public/saved_objects/components/saved_object_finder.js index fd8819449d21d4c..e8e61b4ec67d4be 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.js +++ b/src/ui/public/saved_objects/components/saved_object_finder.js @@ -120,7 +120,7 @@ export class SavedObjectFinder extends React.Component { resp.savedObjects = resp.savedObjects.filter(savedObject => { const typeName = JSON.parse(savedObject.attributes.visState).type; const visType = this.props.visTypes.byName[typeName]; - return visType.stage !== 'lab'; + return visType.stage !== 'experimental'; }); } diff --git a/src/ui/public/saved_objects/show_saved_object_save_modal.js b/src/ui/public/saved_objects/show_saved_object_save_modal.js index f53f819c697f5e0..5cb1681c082826d 100644 --- a/src/ui/public/saved_objects/show_saved_object_save_modal.js +++ b/src/ui/public/saved_objects/show_saved_object_save_modal.js @@ -19,6 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; export function showSaveModal(saveModal) { const container = document.createElement('div'); @@ -44,5 +45,6 @@ export function showSaveModal(saveModal) { onClose: closeModal } ); - ReactDOM.render(element, container); + + ReactDOM.render({element}, container); } diff --git a/src/ui/public/styles/table.less b/src/ui/public/styles/table.less index 30db0c0da8e897f..37dc2f27b408341 100644 --- a/src/ui/public/styles/table.less +++ b/src/ui/public/styles/table.less @@ -79,8 +79,4 @@ table { } } } - - .slice { - stroke: #272727; - } } diff --git a/src/ui/public/styles/variables/for-theme.less b/src/ui/public/styles/variables/for-theme.less index 935e8851db9aff7..5275cd582a752bc 100644 --- a/src/ui/public/styles/variables/for-theme.less +++ b/src/ui/public/styles/variables/for-theme.less @@ -193,32 +193,7 @@ @alert-color: @white; -// Truncate ==================================================================== -@truncate-color: @body-bg; - - -// Typeahead =================================================================== -@typeahead-items-border: @globalColorLightGray; -@typeahead-items-color: @text-color; -@typeahead-items-bg: @body-bg; - -@typeahead-item-color: @text-color; -@typeahead-item-border: @globalColorLightGray; -@typeahead-item-bg: @body-bg; -@typeahead-item-active-bg: @globalColorBlue; - - -// Alerts ====================================================================== -@alert-vis-alert-color: @white; -@alert-vis-alert-border: @white; - - -// Legend ====================================================================== -@legend-item-color: #666; - // Tooltip ===================================================================== -@tooltip-space: 8px; -@tooltip-space-tight: @tooltip-space / 2; @tooltip-bg: fadeout(darken(@globalColorBlue, 30%), 10%); @tooltip-color: @globalColorWhite; @@ -227,17 +202,6 @@ @tooltip-bold: 600; - -// Svg ========================================================================= -@svg-axis-color: #ddd; -@svg-tick-text-color: #848e96; -@svg-brush-color: @white; -@svg-endzone-bg: @black; - -@vis-axis-title-color: #848e96; -@vis-chart-title-color: #848e96; - - // Saved Object Finder ========================================================= @saved-object-finder-link-color: @link-color; @saved-object-finder-icon-color: darken(@saved-object-finder-link-color, 10%); diff --git a/src/ui/public/timefilter/timefilter.js b/src/ui/public/timefilter/timefilter.js index ade9a4801d52a5e..1df5473df96118b 100644 --- a/src/ui/public/timefilter/timefilter.js +++ b/src/ui/public/timefilter/timefilter.js @@ -37,7 +37,12 @@ class Timefilter extends SimpleEmitter { } getTime = () => { - return _.clone(this._time); + const { from, to } = this._time; + return { + ...this._time, + from: moment.isMoment(from) ? from.toISOString() : from, + to: moment.isMoment(to) ? to.toISOString() : to + }; } /** diff --git a/src/ui/public/timefilter/timefilter.test.js b/src/ui/public/timefilter/timefilter.test.js index 7071a6c7f7a707e..f35f5addaa2506a 100644 --- a/src/ui/public/timefilter/timefilter.test.js +++ b/src/ui/public/timefilter/timefilter.test.js @@ -48,6 +48,7 @@ jest.mock('ui/timefilter/lib/parse_querystring', import sinon from 'sinon'; import expect from 'expect.js'; +import moment from 'moment'; import { timefilter } from './timefilter'; function stubNowTime(nowTime) { @@ -101,6 +102,17 @@ describe('setTime', () => { expect(update.called).to.be(true); expect(fetch.called).to.be(true); }); + + test('should return strings and not moment objects', () => { + const from = moment().subtract(15, 'minutes'); + const to = moment(); + timefilter.setTime({ to, from, mode: 'absolute' }); + expect(timefilter.getTime()).to.eql({ + from: from.toISOString(), + to: to.toISOString(), + mode: 'absolute' + }); + }); }); describe('setRefreshInterval', () => { diff --git a/src/ui/public/utils/__tests__/brush_event.test.js b/src/ui/public/utils/__tests__/brush_event.test.js index 701586bad92557b..f75b91f91721901 100644 --- a/src/ui/public/utils/__tests__/brush_event.test.js +++ b/src/ui/public/utils/__tests__/brush_event.test.js @@ -38,7 +38,6 @@ jest.mock('ui/chrome', import _ from 'lodash'; import expect from 'expect.js'; -import moment from 'moment'; import { onBrushEvent } from '../brush_event'; import { timefilter } from 'ui/timefilter'; @@ -102,17 +101,12 @@ describe('brushEvent', () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; onBrushEvent(event, $state); - expect(timefilter.getTime().mode).to.be('absolute'); - expect(moment.isMoment(timefilter.getTime().from)) - .to.be(true); + const { mode, from, to } = timefilter.getTime(); + expect(mode).to.be('absolute'); // Set to a baseline timezone for comparison. - expect(timefilter.getTime().from.utcOffset(0).format('YYYY-MM-DD')) - .to.equal('2014-01-01'); - expect(moment.isMoment(timefilter.getTime().to)) - .to.be(true); + expect(from).to.be(new Date(JAN_01_2014).toISOString()); // Set to a baseline timezone for comparison. - expect(timefilter.getTime().to.utcOffset(0).format('YYYY-MM-DD')) - .to.equal('2014-01-02'); + expect(to).to.be(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); }); }); diff --git a/src/ui/public/vis/__tests__/_vis.js b/src/ui/public/vis/__tests__/_vis.js index f40e88ec99a7b32..ed1c767137a88d5 100644 --- a/src/ui/public/vis/__tests__/_vis.js +++ b/src/ui/public/vis/__tests__/_vis.js @@ -19,7 +19,6 @@ import _ from 'lodash'; import ngMock from 'ng_mock'; -import sinon from 'sinon'; import expect from 'expect.js'; import { VisProvider } from '..'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; @@ -42,15 +41,6 @@ describe('Vis Class', function () { listeners: { click: _.noop } }; - // Wrap the given vis type definition in a state, that can be passed to vis - const state = (type) => ({ - type: { - visConfig: { defaults: {} }, - schemas: {}, - ...type, - } - }); - beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { Vis = Private(VisProvider); @@ -116,41 +106,4 @@ describe('Vis Class', function () { }); }); - describe('vis addFilter method', () => { - let aggConfig; - let data; - - beforeEach(() => { - aggConfig = { - type: { name: 'terms' }, - params: {}, - createFilter: sinon.stub() - }; - - data = { - columns: [{ - id: 'col-0', - title: 'test', - aggConfig - }], - rows: [{ 'col-0': 'US' }] - }; - }); - - - it('adds a simple filter', () => { - const vis = new Vis(indexPattern, state({ requestHandler: 'none' })); - vis.API.events.addFilter(data, 0, 0); - expect(aggConfig.createFilter.callCount).to.be(1); - expect(aggConfig.createFilter.getCall(0).args[0]).to.be('US'); - }); - - it('adds a filter if value is provided instead of row index', () => { - const vis = new Vis(indexPattern, state({ requestHandler: 'none' })); - vis.API.events.addFilter(data, 0, -1, 'UK'); - expect(aggConfig.createFilter.callCount).to.be(1); - expect(aggConfig.createFilter.getCall(0).args[0]).to.be('UK'); - }); - }); - }); diff --git a/src/ui/public/vis/__tests__/vis_types/base_vis_type.js b/src/ui/public/vis/__tests__/vis_types/base_vis_type.js index b14193795f6d2a8..2a79b40e7496a43 100644 --- a/src/ui/public/vis/__tests__/vis_types/base_vis_type.js +++ b/src/ui/public/vis/__tests__/vis_types/base_vis_type.js @@ -18,9 +18,16 @@ */ import expect from 'expect.js'; -import { BaseVisType } from '../../vis_types/base_vis_type'; +import ngMock from 'ng_mock'; +import { BaseVisTypeProvider } from '../../vis_types/base_vis_type'; describe('Base Vis Type', function () { + let BaseVisType; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + BaseVisType = Private(BaseVisTypeProvider); + })); describe('initialization', () => { it('should throw if mandatory properties are missing', () => { diff --git a/src/ui/public/vis/__tests__/vis_types/react_vis_type.js b/src/ui/public/vis/__tests__/vis_types/react_vis_type.js index ae55d62599cba0a..cf2fbab88081493 100644 --- a/src/ui/public/vis/__tests__/vis_types/react_vis_type.js +++ b/src/ui/public/vis/__tests__/vis_types/react_vis_type.js @@ -18,10 +18,13 @@ */ import expect from 'expect.js'; -import { ReactVisType } from '../../vis_types/react_vis_type'; +import ngMock from 'ng_mock'; +import { ReactVisTypeProvider } from '../../vis_types/react_vis_type'; describe('React Vis Type', function () { + let ReactVisType; + const visConfig = { name: 'test', title: 'test', @@ -31,6 +34,11 @@ describe('React Vis Type', function () { type: { visConfig: { component: 'test' } } }; + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + ReactVisType = Private(ReactVisTypeProvider); + })); + describe('initialization', () => { it('should throw if component is not set', () => { expect(() => { diff --git a/src/ui/public/vis/_index.scss b/src/ui/public/vis/_index.scss index df72751181ac7a0..4fb07557977d31e 100644 --- a/src/ui/public/vis/_index.scss +++ b/src/ui/public/vis/_index.scss @@ -1,3 +1,4 @@ -@import './editors/components/index'; -@import './editors/default/index'; +@import './components/index'; +@import './editors/index'; @import './map/index'; +@import './vis_types/index'; diff --git a/src/ui/public/vis/components/_index.scss b/src/ui/public/vis/components/_index.scss new file mode 100644 index 000000000000000..0d79aa9c458acae --- /dev/null +++ b/src/ui/public/vis/components/_index.scss @@ -0,0 +1 @@ +@import './tooltip/index'; diff --git a/src/ui/public/vis/components/tooltip/_index.scss b/src/ui/public/vis/components/tooltip/_index.scss new file mode 100644 index 000000000000000..f4e7075ff7b4fe2 --- /dev/null +++ b/src/ui/public/vis/components/tooltip/_index.scss @@ -0,0 +1 @@ +@import './tooltip'; diff --git a/src/ui/public/vis/components/tooltip/_tooltip.scss b/src/ui/public/vis/components/tooltip/_tooltip.scss new file mode 100644 index 000000000000000..28b8e485e15ffa7 --- /dev/null +++ b/src/ui/public/vis/components/tooltip/_tooltip.scss @@ -0,0 +1,66 @@ +// EUITODO: Use EuiTooltip or create a tooltip mixin +.visTooltip, +.visTooltip__sizingClone { + @include euiBottomShadow($color: $euiColorFullShade); + @include euiFontSizeXS; + visibility: hidden; + pointer-events: none; + position: fixed; + z-index: $euiZLevel9; + background-color: tintOrShade($euiColorFullShade, 25%, 90%); + color: $euiColorGhost; + border-radius: $euiBorderRadius; + max-width: $euiSizeXL * 10; + overflow: hidden; + overflow-wrap: break-word; + + > :last-child { + margin-bottom: $euiSizeS; + } + + > * { + margin: $euiSizeS $euiSizeS 0; + } + + table { + td, + th { + padding: $euiSizeXS; + } + } +} + +.visTooltip__header { + margin: 0 0 $euiSizeS 0; + padding: $euiSizeXS $euiSizeS; + display: flex; + align-items: center; + + &:last-child { + margin-bottom: 0; + } + + + * { + margin-top: $euiSizeS; + } +} + +.visTooltip__headerIcon { + flex: 0 0 auto; + padding-right: $euiSizeS; +} + +.visTooltip__headerText { + flex: 1 1 100%; +} + +.visTooltip__label { + // max-width: $euiSizeXL * 3; + font-weight: $euiFontWeightMedium; + color: shade($euiColorGhost, 20%); +} + +.visTooltip__sizingClone { + top: -500px; + left: -500px; +} diff --git a/src/ui/public/vis/components/tooltip/tooltip.js b/src/ui/public/vis/components/tooltip/tooltip.js index 15f2d9b6caaf864..6008d5cf7961ab2 100644 --- a/src/ui/public/vis/components/tooltip/tooltip.js +++ b/src/ui/public/vis/components/tooltip/tooltip.js @@ -44,9 +44,9 @@ export function Tooltip(id, el, formatter, events) { this.order = 100; // higher ordered contents are rendered below the others this.formatter = formatter; this.events = events; - this.containerClass = 'vis-wrapper'; - this.tooltipClass = 'vis-tooltip'; - this.tooltipSizerClass = 'vis-tooltip-sizing-clone'; + this.containerClass = 'visWrapper'; + this.tooltipClass = 'visTooltip'; + this.tooltipSizerClass = 'visTooltip__sizingClone'; this.showCondition = _.constant(true); this.binder = new Binder(); diff --git a/src/ui/public/vis/editors/_index.scss b/src/ui/public/vis/editors/_index.scss new file mode 100644 index 000000000000000..da6e0bd8837e856 --- /dev/null +++ b/src/ui/public/vis/editors/_index.scss @@ -0,0 +1,2 @@ +@import './components/index'; +@import './default/index'; diff --git a/src/ui/public/vis/editors/default/_default.scss b/src/ui/public/vis/editors/default/_default.scss index b1de3b8343e21ab..40fca7d4446254a 100644 --- a/src/ui/public/vis/editors/default/_default.scss +++ b/src/ui/public/vis/editors/default/_default.scss @@ -106,7 +106,7 @@ flex: 1 1 100%; } - .visualize-chart { + .visChart { position: relative; } } diff --git a/src/ui/public/vis/lib/timezone.js b/src/ui/public/vis/lib/timezone.js index 473be70aa791df0..670e47da849e815 100644 --- a/src/ui/public/vis/lib/timezone.js +++ b/src/ui/public/vis/lib/timezone.js @@ -17,14 +17,13 @@ * under the License. */ -const tzDetect = require('jstimezonedetect').jstz; -import moment from 'moment'; +import moment from 'moment-timezone'; export function timezoneProvider(config) { return function () { if (config.isDefault('dateFormat:tz')) { - const detectedTimezone = tzDetect.determine().name(); + const detectedTimezone = moment.tz.guess(); if (detectedTimezone) return detectedTimezone; else return moment().format('Z'); } else { diff --git a/src/ui/public/vis/map/_leaflet_overrides.scss b/src/ui/public/vis/map/_leaflet_overrides.scss index e54c2dd092a4e6a..c2c33b1c26f6b87 100644 --- a/src/ui/public/vis/map/_leaflet_overrides.scss +++ b/src/ui/public/vis/map/_leaflet_overrides.scss @@ -18,6 +18,13 @@ $visMapLeafletSprite: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/s min-height: 1px !important; } +.leaflet-clickable { + &:hover { + stroke-width: $euiSizeS; + stroke-opacity: 0.8; + } +} + /** * 1. Since Leaflet is an external library, we also have to provide EUI variables * to non-override colors for darkmode. @@ -56,7 +63,7 @@ $visMapLeafletSprite: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/s padding: 0; background: $temp-euiTooltipBackground; color: $temp-euiTooltipText; - border-radius: $euiBorderRadius; + border-radius: $euiBorderRadius !important; // Override all positions the popup might be at } .leaflet-popup { @@ -82,10 +89,6 @@ $visMapLeafletSprite: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/s table { td,th { padding: $euiSizeXS; - - &.row-bucket { - word-break: break-all; - } } } } diff --git a/src/ui/public/vis/vis.js b/src/ui/public/vis/vis.js index 16b92fce74533ab..05c2d3c79ee18fc 100644 --- a/src/ui/public/vis/vis.js +++ b/src/ui/public/vis/vis.js @@ -32,20 +32,17 @@ import _ from 'lodash'; import { VisTypesRegistryProvider } from '../registry/vis_types'; import { AggConfigs } from './agg_configs'; import { PersistedState } from '../persisted_state'; -import { onBrushEvent } from '../utils/brush_event'; import { FilterBarQueryFilterProvider } from '../filter_bar/query_filter'; import { updateVisualizationConfig } from './vis_update'; import { SearchSourceProvider } from '../courier/search_source'; import { SavedObjectsClientProvider } from '../saved_objects'; import { timefilter } from 'ui/timefilter'; -import { VisFiltersProvider } from './vis_filters'; export function VisProvider(Private, indexPatterns, getAppState) { const visTypes = Private(VisTypesRegistryProvider); const queryFilter = Private(FilterBarQueryFilterProvider); const SearchSource = Private(SearchSourceProvider); const savedObjectsClient = Private(SavedObjectsClientProvider); - const visFilter = Private(VisFiltersProvider); class Vis extends EventEmitter { constructor(indexPattern, visState) { @@ -73,14 +70,8 @@ export function VisProvider(Private, indexPatterns, getAppState) { timeFilter: timefilter, queryFilter: queryFilter, events: { - // the filter method will be removed in the near feature - // you should rather use addFilter method below - filter: visFilter.filter, - createFilter: visFilter.createFilter, - addFilter: visFilter.addFilter, - brush: (event) => { - onBrushEvent(event, getAppState()); - } + filter: data => this.eventsSubject.next({ name: 'filterBucket', data }), + brush: data => this.eventsSubject.next({ name: 'brush', data }), }, getAppState, }; diff --git a/src/ui/public/vis/vis_factory.js b/src/ui/public/vis/vis_factory.js index 94e6d429f89d685..fba76f5af019bec 100644 --- a/src/ui/public/vis/vis_factory.js +++ b/src/ui/public/vis/vis_factory.js @@ -17,11 +17,13 @@ * under the License. */ -import { BaseVisType, AngularVisTypeProvider, ReactVisType, VislibVisTypeProvider } from './vis_types'; +import { BaseVisTypeProvider, AngularVisTypeProvider, ReactVisTypeProvider, VislibVisTypeProvider } from './vis_types'; export const VisFactoryProvider = (Private) => { const AngularVisType = Private(AngularVisTypeProvider); const VislibVisType = Private(VislibVisTypeProvider); + const BaseVisType = Private(BaseVisTypeProvider); + const ReactVisType = Private(ReactVisTypeProvider); return { createBaseVisualization: (config) => { diff --git a/src/ui/public/vis/vis_filters.js b/src/ui/public/vis/vis_filters.js index fe24dacb61560be..86ef72f51cbd439 100644 --- a/src/ui/public/vis/vis_filters.js +++ b/src/ui/public/vis/vis_filters.js @@ -20,6 +20,7 @@ import _ from 'lodash'; import { FilterBarPushFiltersProvider } from '../filter_bar/push_filters'; import { FilterBarQueryFilterProvider } from '../filter_bar/query_filter'; +import { onBrushEvent } from '../utils/brush_event'; const getTerms = (table, columnIndex, rowIndex) => { if (rowIndex === -1) { @@ -41,26 +42,26 @@ const getTerms = (table, columnIndex, rowIndex) => { }))]; }; -export function VisFiltersProvider(Private, getAppState) { +const createFilter = (data, columnIndex, rowIndex, cellValue) => { + const { aggConfig, id: columnId } = data.columns[columnIndex]; + let filter = []; + const value = rowIndex > -1 ? data.rows[rowIndex][columnId] : cellValue; + if (value === null || value === undefined) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getTerms(data, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + return filter; +}; + +const VisFiltersProvider = (Private, getAppState) => { const filterBarPushFilters = Private(FilterBarPushFiltersProvider); const queryFilter = Private(FilterBarQueryFilterProvider); - const createFilter = (data, columnIndex, rowIndex, cellValue) => { - const { aggConfig, id: columnId } = data.columns[columnIndex]; - let filter = []; - const value = rowIndex > -1 ? data.rows[rowIndex][columnId] : cellValue; - if (value === null || value === undefined) { - return; - } - if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { - const terms = getTerms(data, columnIndex, rowIndex); - filter = aggConfig.createFilter(value, { terms }); - } else { - filter = aggConfig.createFilter(value); - } - return filter; - }; - const filter = (event, { simulate } = {}) => { let data = event.datum.aggConfigResult; const filters = []; @@ -87,14 +88,18 @@ export function VisFiltersProvider(Private, getAppState) { return filters; }; - const addFilter = (data, columnIndex, rowIndex, cellValue) => { - const filter = createFilter(data, columnIndex, rowIndex, cellValue); + const addFilter = (event) => { + const filter = createFilter(event.table, event.column, event.row, event.value); queryFilter.addFilters(filter); }; return { - createFilter, addFilter, - filter + filter, + brush: (event) => { + onBrushEvent(event, getAppState()); + }, }; -} +}; + +export { VisFiltersProvider, createFilter }; diff --git a/src/ui/public/vis/vis_types/_index.scss b/src/ui/public/vis/vis_types/_index.scss new file mode 100644 index 000000000000000..9d86383ec40b2e5 --- /dev/null +++ b/src/ui/public/vis/vis_types/_index.scss @@ -0,0 +1,2 @@ +@import './vislib_vis_type'; +@import './vislib_vis_legend'; diff --git a/src/ui/public/vis/vis_types/_vislib_vis_legend.scss b/src/ui/public/vis/vis_types/_vislib_vis_legend.scss new file mode 100644 index 000000000000000..4b2d9508dd535ac --- /dev/null +++ b/src/ui/public/vis/vis_types/_vislib_vis_legend.scss @@ -0,0 +1,112 @@ +@import '../../vislib/variables'; + +// NOTE: Some of the styles attempt to align with the TSVB legend + +.visLegend__toggle { + align-self: flex-start; + // Override .kuiCollapseButton + color: $visValueColor !important; +} + +.visLegend { + @include euiFontSizeXS; + display: flex; + flex-direction: row; + padding: $euiSizeXS 0; + overflow: auto; + min-height: 0; + height: 100%; + overflow: hidden; + + // flex-direction re-aligns toggle button + .visLib--legend-left & { + flex-direction: row-reverse; + } + + .visLib--legend-right & { + flex-direction: row; + } + + .visLib--legend-top & { + flex-direction: column-reverse; + width: 100%; + padding-left: $euiSizeL; + + } + .visLib--legend-bottom & { + flex-direction: column; + width: 100%; + padding-left: $euiSizeL; + } + + .visLegend__list { + width: 150px; // Must be a hard-coded width for the chart to get its correct dimensions + flex: 1 1 auto; + flex-direction: column; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + + .visLib--legend-top &, + .visLib--legend-bottom & { + width: auto; + overflow-y: hidden; + + .visLegend__value { + display: inline-block; + } + } + + &.hidden { + visibility: hidden; + } + } +} + +.visLegend__value { + cursor: pointer; + padding: $euiSizeXS; + display: flex; + + > * { + max-width: 100%; + } + + &.disabled { + opacity: 0.5; + } +} + +.visLegend__valueTitle { + color: $visTextColor; + + &:hover { + text-decoration: underline; + } +} + +.visLegend__valueTitle--truncate { + @include euiTextTruncate; +} + +.visLegend__valueTitle--full { + word-break: break-all; +} + +.visLegend__valueDetails { + border-bottom: 1px solid $euiColorLightShade; + padding-bottom: $euiSizeXS; +} + +.visLegend__valueColorPicker { + width: $euiSizeM * 10; + margin: auto; + + .visLegend__valueColorPickerDot { + margin: $euiSizeXS / 2; + + &:hover { + transform: scale(1.4); + } + } +} diff --git a/src/ui/public/vis/vis_types/_vislib_vis_type.scss b/src/ui/public/vis/vis_types/_vislib_vis_type.scss new file mode 100644 index 000000000000000..c1f369d3e3536f3 --- /dev/null +++ b/src/ui/public/vis/vis_types/_vislib_vis_type.scss @@ -0,0 +1,26 @@ +.visLib { + flex: 1 1 0; + display: flex; + flex-direction: row; + overflow: auto; + + &.visLib--legend-left { + flex-direction: row-reverse; + } + &.visLib--legend-right { + flex-direction: row; + } + &.visLib--legend-top { + flex-direction: column-reverse; + } + &.visLib--legend-bottom { + flex-direction: column; + } +} + +.visLib__chart { + display: flex; + flex: 1 1 auto; + min-height: 0; + min-width: 0; +} diff --git a/src/ui/public/vis/vis_types/angular_vis_type.js b/src/ui/public/vis/vis_types/angular_vis_type.js index cde6dbc5010bfcc..52ee51fb231211c 100644 --- a/src/ui/public/vis/vis_types/angular_vis_type.js +++ b/src/ui/public/vis/vis_types/angular_vis_type.js @@ -17,11 +17,12 @@ * under the License. */ -import { BaseVisType } from './base_vis_type'; +import { BaseVisTypeProvider } from './base_vis_type'; import $ from 'jquery'; -export function AngularVisTypeProvider($compile, $rootScope) { +export function AngularVisTypeProvider(Private, $compile, $rootScope) { + const BaseVisType = Private(BaseVisTypeProvider); class AngularVisController { constructor(domeElement, vis) { diff --git a/src/ui/public/vis/vis_types/base_vis_type.js b/src/ui/public/vis/vis_types/base_vis_type.js index 50b546e63c259d3..3b360b75df3e1d4 100644 --- a/src/ui/public/vis/vis_types/base_vis_type.js +++ b/src/ui/public/vis/vis_types/base_vis_type.js @@ -19,65 +19,75 @@ import { CATEGORY } from '../vis_category'; import _ from 'lodash'; +import { VisFiltersProvider } from '../vis_filters'; -export class BaseVisType { - constructor(opts = {}) { +export function BaseVisTypeProvider(Private) { + const visFilters = Private(VisFiltersProvider); - if (!opts.name) { - throw('vis_type must define its name'); - } - if (!opts.title) { - throw('vis_type must define its title'); - } - if (!opts.description) { - throw('vis_type must define its description'); - } - if (!opts.icon && !opts.image && !opts.legacyIcon) { - throw('vis_type must define its icon or image'); - } - if (!opts.visualization) { - throw('vis_type must define visualization controller'); - } + class BaseVisType { + constructor(opts = {}) { - const _defaults = { - // name, title, description, icon, image - category: CATEGORY.OTHER, - visualization: null, // must be a class with render/resize/destroy methods - visConfig: { - defaults: {}, // default configuration - }, - requestHandler: 'courier', // select one from registry or pass a function - responseHandler: 'none', - editor: 'default', - editorConfig: { - collections: {}, // collections used for configuration (list of positions, ...) - }, - options: { // controls the visualize editor - showTimePicker: true, - showQueryBar: true, - showFilterBar: true, - showIndexSelection: true, - hierarchicalData: false // we should get rid of this i guess ? - }, - stage: 'production', - feedbackMessage: '' - }; + if (!opts.name) { + throw('vis_type must define its name'); + } + if (!opts.title) { + throw('vis_type must define its title'); + } + if (!opts.description) { + throw('vis_type must define its description'); + } + if (!opts.icon && !opts.image && !opts.legacyIcon) { + throw('vis_type must define its icon or image'); + } + if (!opts.visualization) { + throw('vis_type must define visualization controller'); + } - _.defaultsDeep(this, opts, _defaults); + const _defaults = { + // name, title, description, icon, image + category: CATEGORY.OTHER, + visualization: null, // must be a class with render/resize/destroy methods + visConfig: { + defaults: {}, // default configuration + }, + requestHandler: 'courier', // select one from registry or pass a function + responseHandler: 'none', + editor: 'default', + editorConfig: { + collections: {}, // collections used for configuration (list of positions, ...) + }, + options: { // controls the visualize editor + showTimePicker: true, + showQueryBar: true, + showFilterBar: true, + showIndexSelection: true, + hierarchicalData: false // we should get rid of this i guess ? + }, + events: { + filterBucket: { + defaultAction: visFilters.addFilter, + } + }, + stage: 'production', + feedbackMessage: '' + }; - this.requiresSearch = this.requestHandler !== 'none'; - } + _.defaultsDeep(this, opts, _defaults); - shouldMarkAsExperimentalInUI() { - //we are not making a distinction in the UI if a plugin is experimental and/or labs. - //we just want to indicate it is special. the current flask icon is sufficient for that. - return this.stage === 'experimental' || this.stage === 'lab'; - } + this.requiresSearch = this.requestHandler !== 'none'; + } - get schemas() { - if (this.editorConfig && this.editorConfig.schemas) { - return this.editorConfig.schemas; + shouldMarkAsExperimentalInUI() { + return this.stage === 'experimental'; + } + + get schemas() { + if (this.editorConfig && this.editorConfig.schemas) { + return this.editorConfig.schemas; + } + return []; } - return []; } + + return BaseVisType; } diff --git a/src/ui/public/vis/vis_types/index.js b/src/ui/public/vis/vis_types/index.js index b483d3a9adea295..8642fda2ffd0134 100644 --- a/src/ui/public/vis/vis_types/index.js +++ b/src/ui/public/vis/vis_types/index.js @@ -17,9 +17,9 @@ * under the License. */ -import { BaseVisType } from './base_vis_type'; +import { BaseVisTypeProvider } from './base_vis_type'; import { AngularVisTypeProvider } from './angular_vis_type'; import { VislibVisTypeProvider } from './vislib_vis_type'; -import { ReactVisType } from './react_vis_type'; +import { ReactVisTypeProvider } from './react_vis_type'; -export { BaseVisType, AngularVisTypeProvider, VislibVisTypeProvider, ReactVisType }; +export { BaseVisTypeProvider, AngularVisTypeProvider, VislibVisTypeProvider, ReactVisTypeProvider }; diff --git a/src/ui/public/vis/vis_types/react_vis_type.js b/src/ui/public/vis/vis_types/react_vis_type.js index b7b41ebed91ad0a..6d3f391c3b9fe23 100644 --- a/src/ui/public/vis/vis_types/react_vis_type.js +++ b/src/ui/public/vis/vis_types/react_vis_type.js @@ -20,44 +20,50 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import chrome from '../../chrome'; -import { BaseVisType } from './base_vis_type'; +import { BaseVisTypeProvider } from './base_vis_type'; -class ReactVisController { - constructor(element, vis) { - this.el = element; - this.vis = vis; - } +export function ReactVisTypeProvider(Private) { + const BaseVisType = Private(BaseVisTypeProvider); - render(visData, updateStatus) { - this.visData = visData; - - return new Promise((resolve) => { - const Component = this.vis.type.visConfig.component; - const config = chrome.getUiSettingsClient(); - render(, this.el); - }); - } + class ReactVisController { + constructor(element, vis) { + this.el = element; + this.vis = vis; + } - destroy() { - unmountComponentAtNode(this.el); + render(visData, updateStatus) { + this.visData = visData; + + return new Promise((resolve) => { + const Component = this.vis.type.visConfig.component; + const config = chrome.getUiSettingsClient(); + render(, this.el); + }); + } + + destroy() { + unmountComponentAtNode(this.el); + } } -} -export class ReactVisType extends BaseVisType { - constructor(opts) { - super({ - ...opts, - visualization: ReactVisController - }); + class ReactVisType extends BaseVisType { + constructor(opts) { + super({ + ...opts, + visualization: ReactVisController + }); - if (!this.visConfig.component) { - throw new Error('Missing component for ReactVisType'); + if (!this.visConfig.component) { + throw new Error('Missing component for ReactVisType'); + } } } + + return ReactVisType; } diff --git a/src/ui/public/vis/vis_types/vislib_vis_legend.html b/src/ui/public/vis/vis_types/vislib_vis_legend.html index 4faa4836145fc72..bfc464e8d8040f6 100644 --- a/src/ui/public/vis/vis_types/vislib_vis_legend.html +++ b/src/ui/public/vis/vis_types/vislib_vis_legend.html @@ -1,8 +1,8 @@ -
+
-
    +
    • -
      +
      -
      +
      -
      +
      diff --git a/src/ui/public/vis/vis_types/vislib_vis_legend.js b/src/ui/public/vis/vis_types/vislib_vis_legend.js index ccb6b3da69884ea..9b17ed9433b4ee9 100644 --- a/src/ui/public/vis/vis_types/vislib_vis_legend.js +++ b/src/ui/public/vis/vis_types/vislib_vis_legend.js @@ -21,12 +21,14 @@ import _ from 'lodash'; import html from './vislib_vis_legend.html'; import { VislibLibDataProvider } from '../../vislib/lib/data'; import { uiModules } from '../../modules'; +import { VisFiltersProvider } from '../vis_filters'; import { htmlIdGenerator, keyCodes } from '@elastic/eui'; uiModules.get('kibana') .directive('vislibLegend', function (Private, $timeout, i18n) { const Data = Private(VislibLibDataProvider); + const visFilters = Private(VisFiltersProvider); return { restrict: 'E', @@ -104,7 +106,7 @@ uiModules.get('kibana') }; $scope.canFilter = function (legendData) { - const filters = $scope.vis.API.events.filter({ datum: legendData.values }, { simulate: true }); + const filters = visFilters.filter({ datum: legendData.values }, { simulate: true }); return filters.length; }; diff --git a/src/ui/public/vis/vis_types/vislib_vis_type.js b/src/ui/public/vis/vis_types/vislib_vis_type.js index f264279eee0ce88..15a3f69eda87cc5 100644 --- a/src/ui/public/vis/vis_types/vislib_vis_type.js +++ b/src/ui/public/vis/vis_types/vislib_vis_type.js @@ -24,20 +24,24 @@ import 'plugins/kbn_vislib_vis_types/controls/heatmap_options'; import 'plugins/kbn_vislib_vis_types/controls/gauge_options'; import 'plugins/kbn_vislib_vis_types/controls/point_series'; import './vislib_vis_legend'; -import { BaseVisType } from './base_vis_type'; +import { BaseVisTypeProvider } from './base_vis_type'; import { AggResponsePointSeriesProvider } from '../../agg_response/point_series/point_series'; import VislibProvider from '../../vislib'; +import { VisFiltersProvider } from '../vis_filters'; import $ from 'jquery'; +import { defaultsDeep } from 'lodash'; export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) { const pointSeries = Private(AggResponsePointSeriesProvider); const vislib = Private(VislibProvider); + const visFilters = Private(VisFiltersProvider); + const BaseVisType = Private(BaseVisTypeProvider); const legendClassName = { - top: 'vislib-container--legend-top', - bottom: 'vislib-container--legend-bottom', - left: 'vislib-container--legend-left', - right: 'vislib-container--legend-right', + top: 'visLib--legend-top', + bottom: 'visLib--legend-bottom', + left: 'visLib--legend-left', + right: 'visLib--legend-right', }; class VislibVisController { @@ -47,11 +51,11 @@ export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) { this.$scope = null; this.container = document.createElement('div'); - this.container.className = 'vislib-container'; + this.container.className = 'visLib'; this.el.appendChild(this.container); this.chartEl = document.createElement('div'); - this.chartEl.className = 'vislib-chart'; + this.chartEl.className = 'visLib__chart'; this.container.appendChild(this.chartEl); } @@ -68,7 +72,7 @@ export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) { if (this.vis.params.addLegend) { $(this.container).attr('class', (i, cls) => { - return cls.replace(/vislib-container--legend-\S+/g, ''); + return cls.replace(/visLib--legend-\S+/g, ''); }).addClass(legendClassName[this.vis.params.legendPosition]); this.$scope = $rootScope.$new(); @@ -118,6 +122,14 @@ export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) { if (!opts.responseConverter) { opts.responseConverter = pointSeries; } + opts.events = defaultsDeep({}, opts.events, { + filterBucket: { + defaultAction: visFilters.filter, + }, + brush: { + defaultAction: visFilters.brush, + } + }); opts.visualization = VislibVisController; super(opts); this.refreshLegend = 0; diff --git a/src/ui/public/vislib/__tests__/index.js b/src/ui/public/vislib/__tests__/index.js index dbdd17028aee7e9..cfed27d817063ee 100644 --- a/src/ui/public/vislib/__tests__/index.js +++ b/src/ui/public/vislib/__tests__/index.js @@ -21,7 +21,6 @@ import _ from 'lodash'; import expect from 'expect.js'; import ngMock from 'ng_mock'; -import '../styles/main.less'; import VislibProvider from '..'; describe('Vislib Index Test Suite', function () { diff --git a/src/ui/public/vislib/__tests__/lib/axis/axis.js b/src/ui/public/vislib/__tests__/lib/axis/axis.js index 0f5d2281bec58b2..52add55f50430b2 100644 --- a/src/ui/public/vislib/__tests__/lib/axis/axis.js +++ b/src/ui/public/vislib/__tests__/lib/axis/axis.js @@ -109,7 +109,7 @@ describe('Vislib Axis Class Test Suite', function () { VisConfig = Private(VislibVisConfigProvider); el = d3.select('body').append('div') - .attr('class', 'x-axis-wrapper') + .attr('class', 'visAxis--x') .style('height', '40px'); fixture = el.append('div') diff --git a/src/ui/public/vislib/__tests__/lib/axis_title.js b/src/ui/public/vislib/__tests__/lib/axis_title.js index 9bd9cc91537f07c..0541261c089ad5d 100644 --- a/src/ui/public/vislib/__tests__/lib/axis_title.js +++ b/src/ui/public/vislib/__tests__/lib/axis_title.js @@ -107,17 +107,17 @@ describe('Vislib AxisTitle Class Test Suite', function () { PersistedState = $injector.get('PersistedState'); el = d3.select('body').append('div') - .attr('class', 'vis-wrapper'); + .attr('class', 'visWrapper'); el.append('div') - .attr('class', 'axis-wrapper-bottom') + .attr('class', 'visAxis__column--bottom') .append('div') .attr('class', 'axis-title y-axis-title') .style('height', '20px') .style('width', '20px'); el.append('div') - .attr('class', 'axis-wrapper-left') + .attr('class', 'visAxis__column--left') .append('div') .attr('class', 'axis-title x-axis-title') .style('height', '20px') diff --git a/src/ui/public/vislib/__tests__/lib/chart_title.js b/src/ui/public/vislib/__tests__/lib/chart_title.js index 5955c19a02fc404..e88896d70872d8b 100644 --- a/src/ui/public/vislib/__tests__/lib/chart_title.js +++ b/src/ui/public/vislib/__tests__/lib/chart_title.js @@ -97,7 +97,7 @@ describe('Vislib ChartTitle Class Test Suite', function () { persistedState = new ($injector.get('PersistedState'))(); el = d3.select('body').append('div') - .attr('class', 'vis-wrapper') + .attr('class', 'visWrapper') .datum(data); el.append('div') diff --git a/src/ui/public/vislib/__tests__/lib/layout/layout.js b/src/ui/public/vislib/__tests__/lib/layout/layout.js index 8406cf3bf03c90b..149f2e87cc30223 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/layout.js +++ b/src/ui/public/vislib/__tests__/lib/layout/layout.js @@ -73,16 +73,16 @@ dateHistogramArray.forEach(function (data, i) { describe('createLayout Method', function () { it('should append all the divs', function () { - expect($(vis.el).find('.vis-wrapper').length).to.be(1); - expect($(vis.el).find('.y-axis-col-wrapper').length).to.be(2); - expect($(vis.el).find('.vis-col-wrapper').length).to.be(1); - expect($(vis.el).find('.y-axis-col').length).to.be(2); + expect($(vis.el).find('.visWrapper').length).to.be(1); + expect($(vis.el).find('.visAxis--y').length).to.be(2); + expect($(vis.el).find('.visWrapper__column').length).to.be(1); + expect($(vis.el).find('.visAxis__column--y').length).to.be(2); expect($(vis.el).find('.y-axis-title').length).to.be.above(0); - expect($(vis.el).find('.y-axis-div-wrapper').length).to.be(2); - expect($(vis.el).find('.y-axis-spacer-block').length).to.be(4); - expect($(vis.el).find('.chart-wrapper').length).to.be(numberOfCharts); - expect($(vis.el).find('.x-axis-wrapper').length).to.be(2); - expect($(vis.el).find('.x-axis-div-wrapper').length).to.be(2); + expect($(vis.el).find('.visAxis__splitAxes--y').length).to.be(2); + expect($(vis.el).find('.visAxis__spacer--y').length).to.be(4); + expect($(vis.el).find('.visWrapper__chart').length).to.be(numberOfCharts); + expect($(vis.el).find('.visAxis--x').length).to.be(2); + expect($(vis.el).find('.visAxis__splitAxes--x').length).to.be(2); expect($(vis.el).find('.x-axis-title').length).to.be.above(0); }); }); @@ -147,7 +147,7 @@ dateHistogramArray.forEach(function (data, i) { describe('appendElem Method', function () { beforeEach(function () { vis.handler.layout.appendElem(vis.el, 'svg', 'column'); - vis.handler.layout.appendElem('.visualize-chart', 'div', 'test'); + vis.handler.layout.appendElem('.visChart', 'div', 'test'); }); it('should append DOM element to el with a class name', function () { diff --git a/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js b/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js index 4b8f242517ed36c..185597f1ac3c148 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js +++ b/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js @@ -186,7 +186,7 @@ describe('Vislib Split Function Test Suite', function () { }); it('should add the correct class name', function () { - expect(!!$('.chart-wrapper-row').length).to.be(true); + expect(!!$('.visWrapper__splitCharts--row').length).to.be(true); }); }); @@ -196,20 +196,20 @@ describe('Vislib Split Function Test Suite', function () { let fixture; beforeEach(ngMock.inject(function () { - visEl = el.append('div').attr('class', 'vis-wrapper'); - visEl.append('div').attr('class', 'x-axis-chart-title'); - visEl.append('div').attr('class', 'y-axis-chart-title'); - visEl.select('.x-axis-chart-title').call(chartTitleSplit); - visEl.select('.y-axis-chart-title').call(chartTitleSplit); + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); newEl = d3.select('body').append('div') - .attr('class', 'vis-wrapper') + .attr('class', 'visWrapper') .datum({ series: [] }); - newEl.append('div').attr('class', 'x-axis-chart-title'); - newEl.append('div').attr('class', 'y-axis-chart-title'); - newEl.select('.x-axis-chart-title').call(chartTitleSplit); - newEl.select('.y-axis-chart-title').call(chartTitleSplit); + newEl.append('div').attr('class', 'visAxis__splitTitles--x'); + newEl.append('div').attr('class', 'visAxis__splitTitles--y'); + newEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + newEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); fixture = newEl.selectAll(this.childNodes)[0].length; })); @@ -223,8 +223,8 @@ describe('Vislib Split Function Test Suite', function () { }); it('should remove the correct div', function () { - expect($('.y-axis-chart-title').length).to.be(1); - expect($('.x-axis-chart-title').length).to.be(0); + expect($('.visAxis__splitTitles--y').length).to.be(1); + expect($('.visAxis__splitTitles--x').length).to.be(0); }); it('should remove all chart title divs when only one chart is rendered', function () { diff --git a/src/ui/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js b/src/ui/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js index cb849bc6bf601bd..2fe0cc2b7fbeaaf 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js +++ b/src/ui/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js @@ -180,7 +180,7 @@ describe('Vislib Gauge Split Function Test Suite', function () { }); it('should add the correct class name', function () { - expect(!!$('.chart-wrapper-row').length).to.be(true); + expect(!!$('.visWrapper__splitCharts--row').length).to.be(true); }); }); @@ -188,11 +188,11 @@ describe('Vislib Gauge Split Function Test Suite', function () { let visEl; beforeEach(ngMock.inject(function () { - visEl = el.append('div').attr('class', 'vis-wrapper'); - visEl.append('div').attr('class', 'x-axis-chart-title'); - visEl.append('div').attr('class', 'y-axis-chart-title'); - visEl.select('.x-axis-chart-title').call(chartTitleSplit); - visEl.select('.y-axis-chart-title').call(chartTitleSplit); + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); })); @@ -201,8 +201,8 @@ describe('Vislib Gauge Split Function Test Suite', function () { }); it('should append the correct number of divs', function () { - expect($('.x-axis-chart-title .chart-title').length).to.be(2); - expect($('.y-axis-chart-title .chart-title').length).to.be(2); + expect($('.visAxis__splitTitles--x .chart-title').length).to.be(2); + expect($('.visAxis__splitTitles--y .chart-title').length).to.be(2); }); diff --git a/src/ui/public/vislib/__tests__/lib/vis_config.js b/src/ui/public/vislib/__tests__/lib/vis_config.js index 66f3651883e25e8..510263b37bce50b 100644 --- a/src/ui/public/vislib/__tests__/lib/vis_config.js +++ b/src/ui/public/vislib/__tests__/lib/vis_config.js @@ -90,7 +90,7 @@ describe('Vislib VisConfig Class Test Suite', function () { const VisConfig = Private(VislibVisConfigProvider); const PersistedState = $injector.get('PersistedState'); el = d3.select('body').append('div') - .attr('class', 'vis-wrapper') + .attr('class', 'visWrapper') .node(); visConfig = new VisConfig({ diff --git a/src/ui/public/vislib/__tests__/lib/x_axis.js b/src/ui/public/vislib/__tests__/lib/x_axis.js index 183e1b7fc11639d..ca8c64039aa7a5a 100644 --- a/src/ui/public/vislib/__tests__/lib/x_axis.js +++ b/src/ui/public/vislib/__tests__/lib/x_axis.js @@ -102,7 +102,7 @@ describe('Vislib xAxis Class Test Suite', function () { VisConfig = Private(VislibVisConfigProvider); el = d3.select('body').append('div') - .attr('class', 'x-axis-wrapper') + .attr('class', 'visAxis--x') .style('height', '40px'); fixture = el.append('div') diff --git a/src/ui/public/vislib/__tests__/vis.js b/src/ui/public/vislib/__tests__/vis.js index 9f996a82d2c392d..acacf59fbfcdf28 100644 --- a/src/ui/public/vislib/__tests__/vis.js +++ b/src/ui/public/vislib/__tests__/vis.js @@ -105,11 +105,11 @@ dataArray.forEach(function (data, i) { }); it('should remove all DOM elements from el', function () { - expect($(secondVis.el).find('.vis-wrapper').length).to.be(0); + expect($(secondVis.el).find('.visWrapper').length).to.be(0); }); it('should not remove visualizations that have not been destroyed', function () { - expect($(vis.el).find('.vis-wrapper').length).to.be(1); + expect($(vis.el).find('.visWrapper').length).to.be(1); }); }); diff --git a/src/ui/public/vislib/__tests__/visualizations/gauge_chart.js b/src/ui/public/vislib/__tests__/visualizations/gauge_chart.js index 50fea1597eb1073..09d64beb95db3d6 100644 --- a/src/ui/public/vislib/__tests__/visualizations/gauge_chart.js +++ b/src/ui/public/vislib/__tests__/visualizations/gauge_chart.js @@ -77,7 +77,7 @@ describe('Vislib Gauge Chart Test Suite', function () { const config = _.defaultsDeep({}, opts, visLibParams); if (vis) { vis.destroy(); - $('.visualize-chart').remove(); + $('.visChart').remove(); } vis = vislibVis(config); persistedState = new PersistedState(); @@ -95,7 +95,7 @@ describe('Vislib Gauge Chart Test Suite', function () { afterEach(function () { vis.destroy(); - $('.visualize-chart').remove(); + $('.visChart').remove(); }); it('creates meter gauge', function () { diff --git a/src/ui/public/vislib/__tests__/visualizations/pie_chart.js b/src/ui/public/vislib/__tests__/visualizations/pie_chart.js index e8100b1b7fab413..db3593f997cad95 100644 --- a/src/ui/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/ui/public/vislib/__tests__/visualizations/pie_chart.js @@ -143,8 +143,8 @@ describe('No global chart settings', function () { }); it('should render chart titles for all charts', function () { - expect($(chart1.el).find('.y-axis-chart-title').length).to.be(1); - expect($(chart2.el).find('.x-axis-chart-title').length).to.be(1); + expect($(chart1.el).find('.visAxis__splitTitles--y').length).to.be(1); + expect($(chart2.el).find('.visAxis__splitTitles--x').length).to.be(1); }); describe('_validatePieData method', function () { diff --git a/src/ui/public/vislib/_index.scss b/src/ui/public/vislib/_index.scss new file mode 100644 index 000000000000000..f8045b7cf5d351a --- /dev/null +++ b/src/ui/public/vislib/_index.scss @@ -0,0 +1,3 @@ +@import './variables'; + +@import './lib/index'; diff --git a/src/ui/public/vislib/_variables.scss b/src/ui/public/vislib/_variables.scss new file mode 100644 index 000000000000000..58f70eb4848e511 --- /dev/null +++ b/src/ui/public/vislib/_variables.scss @@ -0,0 +1,5 @@ +// TODO: Use the same styles for TSVB and Vislib vis' +$visLineColor: transparentize($euiColorFullShade,0.8); +$visTextColor: transparentize($euiColorFullShade,0.6); +$visValueColor: transparentize($euiColorFullShade,0.3); +$visHoverBackgroundColor: transparentize($euiColorFullShade,0.9); diff --git a/src/ui/public/vislib/lib/_alerts.scss b/src/ui/public/vislib/lib/_alerts.scss new file mode 100644 index 000000000000000..e99fcf6f52b7440 --- /dev/null +++ b/src/ui/public/vislib/lib/_alerts.scss @@ -0,0 +1,55 @@ + +.visAlerts__tray { + position: absolute; + bottom: ($euiSizeXS+1px); + left: 0; + right: 0; + list-style: none; + padding: 0; + + transition-property: opacity; + transition-delay: $euiAnimSpeedExtraFast; + transition-duration: $euiAnimSpeedExtraFast; +} + +.visAlerts__icon { + margin: 0; + padding: 0 $euiSizeS; + flex: 0 0 auto; + align-self: center; +} + +.visAlerts__text { + flex: 1 1 auto; + margin: 0; + padding: 0; +} + +.visAlerts__close { + cursor: pointer; +} + +.visAlert { + margin: 0 $euiSizeS $euiSizeS; + padding: $euiSizeXS $euiSizeS $euiSizeXS $euiSizeXS; + display: flex; +} + +// Modifier naming and colors. +$visAlertTypes: ( + info: $euiColorPrimary, + success: $euiColorSecondary, + warning: $euiColorWarning, + danger: $euiColorDanger, +); + +// Create button modifiders based upon the map. +@each $name, $color in $visAlertTypes { + .visAlert--#{$name} { + $backgroundColor: tintOrShade($color, 90%, 70%); + $textColor: makeHighContrastColor($color, $backgroundColor); + + background-color: $backgroundColor; + color: $textColor; + } +} diff --git a/src/ui/public/vislib/lib/_handler.scss b/src/ui/public/vislib/lib/_handler.scss new file mode 100644 index 000000000000000..8d9a9e35f7e0e22 --- /dev/null +++ b/src/ui/public/vislib/lib/_handler.scss @@ -0,0 +1,21 @@ +.visError { + flex: 1 1 0; + display: flex; + align-items: center; + justify-content: center; + + // From ML + .top { align-self: flex-start; } + .bottom { align-self: flex-end; } + + p { + margin-top: 15%; + @include euiFontSizeL; + } +} + +// Prevent large request errors from overflowing the container +.visError--request { + max-width: 100%; + max-height: 100%; +} diff --git a/src/ui/public/vislib/lib/_index.scss b/src/ui/public/vislib/lib/_index.scss new file mode 100644 index 000000000000000..b19c2dfb153b95e --- /dev/null +++ b/src/ui/public/vislib/lib/_index.scss @@ -0,0 +1,4 @@ +@import './alerts'; +@import './handler'; +@import './layout/index'; + diff --git a/src/ui/public/vislib/lib/alerts.js b/src/ui/public/vislib/lib/alerts.js index 9ec6dcd4d7319f1..5f6b360d43d5874 100644 --- a/src/ui/public/vislib/lib/alerts.js +++ b/src/ui/public/vislib/lib/alerts.js @@ -49,12 +49,12 @@ export function VislibLibAlertsProvider() { const icon = alertDef.icon || type; const msg = alertDef.msg; // alert container - const $icon = $('').addClass('vis-alerts-icon fa fa-' + icon); - const $text = $('

      ').addClass('vis-alerts-text').text(msg); + const $icon = $('').addClass('visAlerts__icon fa fa-' + icon); + const $text = $('

      ').addClass('visAlerts__text').text(msg); const $closeIcon = $('').addClass('fa fa-close'); - const $closeDiv = $('

      ').addClass('vis-alerts-close').append($closeIcon); + const $closeDiv = $('
      ').addClass('visAlerts__close').append($closeIcon); - const $alert = $('
      ').addClass('vis-alert vis-alert-' + type).append([$icon, $text, $closeDiv]); + const $alert = $('
      ').addClass('visAlert visAlert--' + type).append([$icon, $text, $closeDiv]); $closeDiv.on('click', () => { $alert.remove(); }); @@ -67,9 +67,9 @@ export function VislibLibAlertsProvider() { const alerts = this.alerts; const vis = this.vis; - $(vis.el).find('.vis-alerts').append($('
      ').addClass('vis-alerts-tray')); + $(vis.el).find('.visWrapper__alerts').append($('
      ').addClass('visAlerts__tray')); if (!alerts.size()) return; - $(vis.el).find('.vis-alerts-tray').append(alerts.value()); + $(vis.el).find('.visAlerts__tray').append(alerts.value()); } // shows new alert @@ -81,13 +81,13 @@ export function VislibLibAlertsProvider() { }; if (this.alertDefs.find(alertDef => alertDef.msg === alert.msg)) return; this.alertDefs.push(alert); - $(vis.el).find('.vis-alerts-tray').append( + $(vis.el).find('.visAlerts__tray').append( this._addAlert(alert) ); } destroy() { - $(this.vis.el).find('.vis-alerts').remove(); + $(this.vis.el).find('.visWrapper__alerts').remove(); } } diff --git a/src/ui/public/vislib/lib/axis/axis.js b/src/ui/public/vislib/lib/axis/axis.js index 87d98aae37ac0b2..16c4f0f662c1598 100644 --- a/src/ui/public/vislib/lib/axis/axis.js +++ b/src/ui/public/vislib/lib/axis/axis.js @@ -164,8 +164,8 @@ export function VislibLibAxisProvider(Private) { .attr('transform', 'translate(1,0)'); } if (config.get('type') === 'value') { - const spacerNodes = $(chartEl).find(`.y-axis-spacer-block-${position}`); - const elHeight = $(chartEl).find(`.axis-wrapper-${position}`).height(); + const spacerNodes = $(chartEl).find(`.visAxis__spacer--y-${position}`); + const elHeight = $(chartEl).find(`.visAxis__column--${position}`).height(); spacerNodes.height(elHeight); } } else { diff --git a/src/ui/public/vislib/lib/axis/axis_config.js b/src/ui/public/vislib/lib/axis/axis_config.js index e2729d393885b27..61e155879095be0 100644 --- a/src/ui/public/vislib/lib/axis/axis_config.js +++ b/src/ui/public/vislib/lib/axis/axis_config.js @@ -26,7 +26,7 @@ export function VislibLibAxisConfigProvider() { const defaults = { show: true, type: 'value', - elSelector: '.axis-wrapper-{pos} .axis-div', + elSelector: '.visAxis__column--{pos} .axis-div', position: 'left', scale: { type: 'linear', @@ -61,7 +61,7 @@ export function VislibLibAxisConfigProvider() { }, title: { text: '', - elSelector: '.axis-wrapper-{pos} .axis-div', + elSelector: '.visAxis__column--{pos} .axis-div', } }; diff --git a/src/ui/public/vislib/lib/handler.js b/src/ui/public/vislib/lib/handler.js index d1cd971b322d185..acc5ac71e929368 100644 --- a/src/ui/public/vislib/lib/handler.js +++ b/src/ui/public/vislib/lib/handler.js @@ -217,7 +217,7 @@ export function VisHandlerProvider(Private) { .append('div') // class name needs `chart` in it for the polling checkSize function // to continuously call render on resize - .attr('class', 'visualize-error chart error') + .attr('class', 'visError chart error') .attr('data-test-subj', 'visLibVisualizeError'); div.append('h4').text(markdownIt.renderInline(message)); diff --git a/src/ui/public/vislib/lib/layout/_index.scss b/src/ui/public/vislib/lib/layout/_index.scss new file mode 100644 index 000000000000000..0820684ccbcf909 --- /dev/null +++ b/src/ui/public/vislib/lib/layout/_index.scss @@ -0,0 +1 @@ +@import './layout'; diff --git a/src/ui/public/vislib/lib/layout/_layout.scss b/src/ui/public/vislib/lib/layout/_layout.scss new file mode 100644 index 000000000000000..d23bf3afc252213 --- /dev/null +++ b/src/ui/public/vislib/lib/layout/_layout.scss @@ -0,0 +1,332 @@ +// Typical layout +// .visWrapper +// .visAxis--y +// .visAxis__spacer--y.visAxis__spacer--y-[position] +// .visAxis__column--y.visAxis__column--[position] +// .visAxis__splitTitles--y +// .visAxis__splitAxes--y +// .visAxis__spacer--y.visAxis__spacer--y-[position] +// .visWrapper__column +// .visAxis--x.visAxis__column--[position] +// .visAxis__splitAxes--x +// .visWrapper__chart +// .visWrapper__alerts +// .visAxis--x.visAxis__column--[position] +// .visAxis__splitAxes--x +// .visAxis__splitTitles--x +// .visAxis--y +// .visAxis__spacer--y.visAxis__spacer--y-[position] +// .visAxis__column--y.visAxis__column--[position] +// .visAxis__splitAxes--y +// .visAxis__spacer--y.visAxis__spacer--y-[position] + +// +// LAYOUT ONLY +// Numbers in here are brittle +// + +.visWrapper { + display: flex; + flex: 1 1 100%; + flex-direction: row; + min-height: 0; + min-width: 0; + overflow: hidden; + padding: ($euiSizeS + 2px) 0; +} + +.visWrapper__column { + display: flex; + flex: 1 0 0px; + flex-direction: column; + min-height: 0; + min-width: 0; +} + +.visWrapper__splitCharts--column { + display: flex; + flex: 1 0 20px; + flex-direction: row; + min-height: 0; + min-width: 0; + + .visWrapper__chart { + margin-top: 0px; + margin-bottom: 0px; + } +} + +.visWrapper__splitCharts--row { + display: flex; + flex-direction: column; + flex: 1 1 100%; + min-height: 0; + min-width: 0; + + .visWrapper__chart { + margin-left: 0px; + margin-right: 0px; + } +} + +.visWrapper__chart { + display: flex; + flex: 1 0 0px; + overflow: visible; + margin: 5px; + min-height: 0; + min-width: 0; +} + +.visWrapper__alerts { + position: relative; +} + +// General Axes + +.visAxis__column--top .axis-div svg { + margin-bottom: -5px; +} + +// Y Axes + +.visAxis--x, +.visAxis--y { + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; +} + +.visAxis--x { + overflow: visible; +} + +.visAxis__spacer--y { + min-height: 0px; +} + +.visAxis__column--y { + display: flex; + flex-direction: row; + flex: 1 0 ($euiSizeXL + $euiSizeXS); + min-height: 0; + min-width: 0; +} + +.visAxis__splitTitles--y { + display: flex; + flex-direction: column; + min-height: $euiSizeM; + min-width: 0; +} + +.visAxis__splitTitles--x { + display: flex; + flex-direction: row; + min-height: 1px; + max-height: $euiSize; + min-width: $euiSize; +} + +.visAxis__splitAxes--x, +.visAxis__splitAxes--y { + display: flex; + flex-direction: column; + min-height: ($euiSize + $euiSizeXS); + min-width: 0; +} + +.visAxis__splitAxes--x { + flex-direction: row; + min-height: 0; +} + + +// +// STYLE +// + +// BEM NOTE: These selectors could not be renamed. +// Most come from an external libray, others are too general for +// search and replace. The SVG itself doesn't have a class, nor +// could it be easily found to apply to all chart types. +// At least wrapping selectors inside .visWrapper will narrow scope. + + +.visWrapper { + svg { + overflow: visible; + } + + // SVG Element Default Styling + rect { + opacity: 1; + + &:hover { + opacity: 0.8; + } + } + + circle { + opacity: 0; + + &:hover { + opacity: 1; + stroke-width: $euiSizeS; + stroke-opacity: 0.8; + } + } + + .label-line { + fill: none; + stroke-width: 2px; + stroke: $visLineColor; + } + + .label-text { + @include fontSize($euiFontSizeXS); + font-weight: $euiFontWeightRegular; + } + + .y-axis-div { + flex: 1 1 $euiSizeL; + min-width: 1px; + min-height: $euiSizeM; + margin: ($euiSizeXS + 1px) 0; + } + + .x-axis-div { + min-height: 0px; + min-width: 1px; + margin: 0px ($euiSizeXS + 1px); + width: 100%; + + svg { + float: left; /* for some reason svg wont get positioned in top left corner of container div without this */ + } + } + + .tick text { + @include fontSize($euiFontSizeXS - 1px); + fill: $visValueColor; + } + + .axis-title text { + @include fontSize($euiFontSizeXS); + font-weight: $euiFontWeightBold; + fill: $visTextColor; + } + + .y-axis-title { + min-height: ($euiSizeM + 2px); + min-width: 1px; + } + + .x-axis-title { + min-width: $euiSize; + } + + .chart-title { + flex: 1 1 100%; + min-height: ($euiSizeM + 2px); + min-width: ($euiSizeM + 2px); + + text { + @include fontSize($euiFontSizeXS - 1px); + fill: $visTextColor; + } + } + + .chart { + flex: 1 1 100%; + min-height: 0; + min-width: 0; + overflow: visible; + + > svg { + display: block; + } + } + + .chart-row, + .chart-column { + flex: 1 1 auto; + min-height: 0; + min-width: 0; + } + + // Needs to come after .y-axis-div + .visWrapper__chart--first { + margin-top: 0px; + margin-left: 0px + } + + .visWrapper__chart--last { + margin-bottom: 0px; + margin-right: 0px; + } + + .axis { + shape-rendering: crispEdges; + stroke-width: 1px; + + line, path { + stroke: $euiBorderColor; + fill: none; + shape-rendering: crispEdges; + } + } + + .chart-label, + .label-text, + .chart-text { + fill: $visValueColor; + } + + /* Brush Styling */ + .brush .extent { + shape-rendering: crispEdges; + fill: $visHoverBackgroundColor; + } + + .visAreaChart__overlapArea { + opacity: 0.8; + } + + .series > path, + .series > rect { + fill-opacity: 0.8; + stroke-opacity: 1; + stroke-width: 0; + } + + .blur_shape { + opacity: 0.3 !important; + } + + .slice { + stroke-width: $euiSizeXS/2; + stroke: $euiColorEmptyShade; + + &:hover { + opacity: 0.8 + } + } + + .line { + circle { + opacity: 1; + + &:hover { + stroke-width: $euiSizeS; + stroke-opacity: 0.8; + } + } + } + + .endzone { + pointer-events: none; + fill: $visHoverBackgroundColor; + } +} diff --git a/src/ui/public/vislib/lib/layout/layout.js b/src/ui/public/vislib/lib/layout/layout.js index 9e6083caedb6ab3..73b1dd6e4b3629c 100644 --- a/src/ui/public/vislib/lib/layout/layout.js +++ b/src/ui/public/vislib/lib/layout/layout.js @@ -90,7 +90,7 @@ export function VislibLibLayoutLayoutProvider(Private) { const position = axis.axisConfig.get('position'); const chartTitle = new ChartTitle(visConfig); - const axisWrapperElement = $(this.el).find(`.axis-wrapper-${position}`); + const axisWrapperElement = $(this.el).find(`.visAxis__column--${position}`); axisWrapperElement.css('visibility', 'hidden'); axis.render(); @@ -103,10 +103,10 @@ export function VislibLibLayoutLayoutProvider(Private) { if (axis.axisConfig.isHorizontal()) { - const spacerNodes = $(this.el).find(`.y-axis-spacer-block-${position}`); + const spacerNodes = $(this.el).find(`.visAxis__spacer--y-${position}`); spacerNodes.height(`${height}px`); } else { - axisWrapperElement.find('.y-axis-div-wrapper').width(`${width}px`); + axisWrapperElement.find('.visAxis__splitAxes--y').width(`${width}px`); } } diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js index c6c77fb14be0f19..0c14ff8ed761aa6 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js @@ -21,7 +21,7 @@ import d3 from 'd3'; export function VislibLibLayoutSplitsColumnChartChartSplitProvider() { /* - * Adds div DOM elements to the `.chart-wrapper` element based on the data layout. + * Adds div DOM elements to the `.visWrapper__chart` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.chart` elements as row objects. */ @@ -30,14 +30,14 @@ export function VislibLibLayoutSplitsColumnChartChartSplitProvider() { const div = d3.select(this) .attr('class', function () { if (data.rows) { - return 'chart-wrapper-row'; + return 'visWrapper__splitCharts--row'; } else if (data.columns) { - return 'chart-wrapper-column'; + return 'visWrapper__splitCharts--column'; } else { if (parent) { - return 'chart-first chart-last chart-wrapper'; + return 'visWrapper__chart--first visWrapper__chart--last visWrapper__chart'; } - return this.className + ' chart-wrapper'; + return this.className + ' visWrapper__chart'; } }); let divClass = ''; @@ -65,12 +65,12 @@ export function VislibLibLayoutSplitsColumnChartChartSplitProvider() { if (fullDivClass !== 'chart') { if (chartsNumber > 1) { if (i === 0) { - fullDivClass += ' chart-first'; + fullDivClass += ' visWrapper__chart--first'; } else if (i === chartsNumber - 1) { - fullDivClass += ' chart-last'; + fullDivClass += ' visWrapper__chart--last'; } } else { - fullDivClass += ' chart-first chart-last'; + fullDivClass += ' visWrapper__chart--first visWrapper__chart--last'; } } return fullDivClass; diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/chart_title_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/chart_title_split.js index c930589b471bf7d..fc6f261ced1ca35 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/chart_title_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/chart_title_split.js @@ -22,8 +22,8 @@ import $ from 'jquery'; export function VislibLibLayoutSplitsColumnChartChartTitleSplitProvider() { /* - * Adds div DOM elements to either the `.y-axis-chart-title` element or the - * `.x-axis-chart-title` element based on the data layout. + * Adds div DOM elements to either the `.visAxis__splitTitles--y` element or the + * `.visAxis__splitTitles--x` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.chart-title` elements as row objects. * if not data.rows or data.columns, return no chart titles @@ -31,7 +31,7 @@ export function VislibLibLayoutSplitsColumnChartChartTitleSplitProvider() { return function (selection) { selection.each(function (data) { const div = d3.select(this); - const parent = $(this).parents('.vis-wrapper'); + const parent = $(this).parents('.visWrapper'); if (!data.series) { div.selectAll('.chart-title') @@ -43,9 +43,9 @@ export function VislibLibLayoutSplitsColumnChartChartTitleSplitProvider() { .attr('class', 'chart-title'); if (data.rows) { - parent.find('.x-axis-chart-title').remove(); + parent.find('.visAxis__splitTitles--x').remove(); } else { - parent.find('.y-axis-chart-title').remove(); + parent.find('.visAxis__splitTitles--y').remove(); } return div; diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js index 77053ceca0f0c82..a57e25fad3377e0 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js @@ -22,7 +22,7 @@ import d3 from 'd3'; export function VislibLibLayoutSplitsColumnChartXAxisSplitProvider() { /* - * Adds div DOM elements to the `.x-axis-div-wrapper` element based on the data layout. + * Adds div DOM elements to the `.visAxis__splitAxes--x` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.x-axis-div` elements as row objects. */ @@ -42,10 +42,10 @@ export function VislibLibLayoutSplitsColumnChartXAxisSplitProvider() { .attr('class', (d, i) => { let divClass = ''; if (i === 0) { - divClass += ' chart-first'; + divClass += ' visWrapper__chart--first'; } if (i === columns - 1) { - divClass += ' chart-last'; + divClass += ' visWrapper__chart--last'; } return 'x-axis-div axis-div' + divClass; }); diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js index 78f300100ab2d29..354a51dcc5feef4 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js @@ -21,7 +21,7 @@ import d3 from 'd3'; export function VislibLibLayoutSplitsColumnChartYAxisSplitProvider() { /* - * Adds div DOM elements to the `.y-axis-div-wrapper` element based on the data layout. + * Adds div DOM elements to the `.visAxis__splitAxes--y` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.y-axis-div` elements as row objects. */ @@ -44,10 +44,10 @@ export function VislibLibLayoutSplitsColumnChartYAxisSplitProvider() { .attr('class', (d, i) => { let divClass = ''; if (i === 0) { - divClass += ' chart-first'; + divClass += ' visWrapper__chart--first'; } if (i === rows - 1) { - divClass += ' chart-last'; + divClass += ' visWrapper__chart--last'; } return 'y-axis-div axis-div' + divClass; }); diff --git a/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_split.js b/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_split.js index f00906c651742c0..906483966dc2236 100644 --- a/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_split.js +++ b/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_split.js @@ -23,7 +23,7 @@ import d3 from 'd3'; export default function ChartSplitFactory() { /* - * Adds div DOM elements to the `.chart-wrapper` element based on the data layout. + * Adds div DOM elements to the `.visWrapper__chart` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.chart` elements as row objects. */ @@ -33,11 +33,11 @@ export default function ChartSplitFactory() { const div = d3.select(this) .attr('class', function () { if (data.rows) { - return 'chart-wrapper-row'; + return 'visWrapper__splitCharts--row'; } else if (data.columns) { - return 'chart-wrapper-column'; + return 'visWrapper__splitCharts--column'; } else { - return 'chart-wrapper'; + return 'visWrapper__chart'; } }); let divClass; diff --git a/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js b/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js index 616d2f1f72dfc35..ad30bf91e9f00e4 100644 --- a/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js +++ b/src/ui/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js @@ -23,8 +23,8 @@ import d3 from 'd3'; export default function ChartTitleSplitFactory() { /* - * Adds div DOM elements to either the `.y-axis-chart-title` element or the - * `.x-axis-chart-title` element based on the data layout. + * Adds div DOM elements to either the `.visAxis__splitTitles--y` element or the + * `.visAxis__splitTitles--x` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.chart-title` elements as row objects. * if not data.rows or data.columns, return no chart titles @@ -45,9 +45,9 @@ export default function ChartTitleSplitFactory() { .attr('class', 'chart-title'); if (data.rows) { - d3.select(parent).select('.x-axis-chart-title').remove(); + d3.select(parent).select('.visAxis__splitTitles--x').remove(); } else { - d3.select(parent).select('.y-axis-chart-title').remove(); + d3.select(parent).select('.visAxis__splitTitles--y').remove(); } return div; diff --git a/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_split.js b/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_split.js index 3cb4a1740659b66..7839c205c180479 100644 --- a/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_split.js +++ b/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_split.js @@ -21,7 +21,7 @@ import d3 from 'd3'; export function VislibLibLayoutSplitsPieChartChartSplitProvider() { /* - * Adds div DOM elements to the `.chart-wrapper` element based on the data layout. + * Adds div DOM elements to the `.visWrapper__chart` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.chart` elements as row objects. */ @@ -31,11 +31,11 @@ export function VislibLibLayoutSplitsPieChartChartSplitProvider() { const div = d3.select(this) .attr('class', function () { if (data.rows) { - return 'chart-wrapper-row'; + return 'visWrapper__splitCharts--row'; } else if (data.columns) { - return 'chart-wrapper-column'; + return 'visWrapper__splitCharts--column'; } else { - return 'chart-wrapper'; + return 'visWrapper__chart'; } }); let divClass; diff --git a/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js b/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js index c94a4b9b9fa0345..c22dafd620cec97 100644 --- a/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js +++ b/src/ui/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js @@ -21,8 +21,8 @@ import d3 from 'd3'; export function VislibLibLayoutSplitsPieChartChartTitleSplitProvider() { /* - * Adds div DOM elements to either the `.y-axis-chart-title` element or the - * `.x-axis-chart-title` element based on the data layout. + * Adds div DOM elements to either the `.visAxis__splitTitles--y` element or the + * `.visAxis__splitTitles--x` element based on the data layout. * For example, if the data has rows, it returns the same number of * `.chart-title` elements as row objects. * if not data.rows or data.columns, return no chart titles @@ -43,9 +43,9 @@ export function VislibLibLayoutSplitsPieChartChartTitleSplitProvider() { .attr('class', 'chart-title'); if (data.rows) { - d3.select(parent).select('.x-axis-chart-title').remove(); + d3.select(parent).select('.visAxis__splitTitles--x').remove(); } else { - d3.select(parent).select('.y-axis-chart-title').remove(); + d3.select(parent).select('.visAxis__splitTitles--y').remove(); } return div; diff --git a/src/ui/public/vislib/lib/layout/types/column_layout.js b/src/ui/public/vislib/lib/layout/types/column_layout.js index 849f011d02423a1..fdac4687083608e 100644 --- a/src/ui/public/vislib/lib/layout/types/column_layout.js +++ b/src/ui/public/vislib/lib/layout/types/column_layout.js @@ -54,75 +54,75 @@ export function VislibLibLayoutTypesColumnLayoutProvider(Private) { { parent: el, type: 'div', - class: 'vis-wrapper', + class: 'visWrapper', datum: data, children: [ { type: 'div', - class: 'y-axis-col-wrapper', + class: 'visAxis--y', children: [ { type: 'div', - class: 'y-axis-spacer-block y-axis-spacer-block-top' + class: 'visAxis__spacer--y visAxis__spacer--y-top' }, { type: 'div', - class: 'y-axis-col axis-wrapper-left', + class: 'visAxis__column--y visAxis__column--left', children: [ { type: 'div', - class: 'y-axis-chart-title', + class: 'visAxis__splitTitles--y', splits: chartTitleSplit }, { type: 'div', - class: 'y-axis-div-wrapper', + class: 'visAxis__splitAxes--y', splits: yAxisSplit } ] }, { type: 'div', - class: 'y-axis-spacer-block y-axis-spacer-block-bottom' + class: 'visAxis__spacer--y visAxis__spacer--y-bottom' } ] }, { type: 'div', - class: 'vis-col-wrapper', + class: 'visWrapper__column', children: [ { type: 'div', - class: 'x-axis-wrapper axis-wrapper-top', + class: 'visAxis--x visAxis__column--top', children: [ { type: 'div', - class: 'x-axis-div-wrapper', + class: 'visAxis__splitAxes--x', splits: xAxisSplit } ] }, { type: 'div', - class: 'chart-wrapper', + class: 'visWrapper__chart', splits: chartSplit }, { type: 'div', - class: 'vis-alerts' + class: 'visWrapper__alerts' }, { type: 'div', - class: 'x-axis-wrapper axis-wrapper-bottom', + class: 'visAxis--x visAxis__column--bottom', children: [ { type: 'div', - class: 'x-axis-div-wrapper', + class: 'visAxis__splitAxes--x', splits: xAxisSplit }, { type: 'div', - class: 'x-axis-chart-title', + class: 'visAxis__splitTitles--x', splits: chartTitleSplit } ] @@ -131,26 +131,26 @@ export function VislibLibLayoutTypesColumnLayoutProvider(Private) { }, { type: 'div', - class: 'y-axis-col-wrapper', + class: 'visAxis--y', children: [ { type: 'div', - class: 'y-axis-spacer-block y-axis-spacer-block-top' + class: 'visAxis__spacer--y visAxis__spacer--y-top' }, { type: 'div', - class: 'y-axis-col axis-wrapper-right', + class: 'visAxis__column--y visAxis__column--right', children: [ { type: 'div', - class: 'y-axis-div-wrapper', + class: 'visAxis__splitAxes--y', splits: yAxisSplit } ] }, { type: 'div', - class: 'y-axis-spacer-block y-axis-spacer-block-bottom' + class: 'visAxis__spacer--y visAxis__spacer--y-bottom' } ] } diff --git a/src/ui/public/vislib/lib/layout/types/gauge_layout.js b/src/ui/public/vislib/lib/layout/types/gauge_layout.js index 1e1b7d104b999aa..745dcd65b15820e 100644 --- a/src/ui/public/vislib/lib/layout/types/gauge_layout.js +++ b/src/ui/public/vislib/lib/layout/types/gauge_layout.js @@ -49,30 +49,30 @@ export function GaugeLayoutProvider(Private) { { parent: el, type: 'div', - class: 'vis-wrapper', + class: 'visWrapper', datum: data, children: [ { type: 'div', - class: 'y-axis-chart-title', + class: 'visAxis__splitTitles--y', //splits: chartTitleSplit }, { type: 'div', - class: 'vis-col-wrapper', + class: 'visWrapper__column', children: [ { type: 'div', - class: 'chart-wrapper', + class: 'visWrapper__chart', splits: chartSplit }, { type: 'div', - class: 'vis-alerts' + class: 'visWrapper__alerts' }, { type: 'div', - class: 'x-axis-chart-title', + class: 'visAxis__splitTitles--x', //splits: chartTitleSplit } ] diff --git a/src/ui/public/vislib/lib/layout/types/pie_layout.js b/src/ui/public/vislib/lib/layout/types/pie_layout.js index 0cf4548d361bb5b..2fe0ad31bc31df1 100644 --- a/src/ui/public/vislib/lib/layout/types/pie_layout.js +++ b/src/ui/public/vislib/lib/layout/types/pie_layout.js @@ -50,26 +50,26 @@ export function VislibLibLayoutTypesPieLayoutProvider(Private) { { parent: el, type: 'div', - class: 'vis-wrapper', + class: 'visWrapper', datum: data, children: [ { type: 'div', - class: 'y-axis-chart-title', + class: 'visAxis__splitTitles--y', splits: chartTitleSplit }, { type: 'div', - class: 'vis-col-wrapper', + class: 'visWrapper__column', children: [ { type: 'div', - class: 'chart-wrapper', + class: 'visWrapper__chart', splits: chartSplit }, { type: 'div', - class: 'x-axis-chart-title', + class: 'visAxis__splitTitles--x', splits: chartTitleSplit } ] diff --git a/src/ui/public/vislib/partials/touchdown.tmpl.html b/src/ui/public/vislib/partials/touchdown.tmpl.html index 102a5dce3ca2a77..ee95eef68f3b27f 100644 --- a/src/ui/public/vislib/partials/touchdown.tmpl.html +++ b/src/ui/public/vislib/partials/touchdown.tmpl.html @@ -1,7 +1,7 @@ -

      - - +

      + + <%= wholeBucket ? 'Part of this bucket' : 'This area' %> - may contain partial data.
      The selected time range does not fully cover it. + may contain partial data. The selected time range does not fully cover it.
      -

      \ No newline at end of file +

      diff --git a/src/ui/public/vislib/styles/_alerts.less b/src/ui/public/vislib/styles/_alerts.less deleted file mode 100644 index c450e09afe6d782..000000000000000 --- a/src/ui/public/vislib/styles/_alerts.less +++ /dev/null @@ -1,61 +0,0 @@ -@import "~ui/styles/variables"; - -.vis-alerts { - position: relative; -} - - .vis-alerts-tray { - position: absolute; - bottom: 5px; - left: 0px; - right: 0px; - list-style: none; - padding: 0; - - transition-property: opacity; - transition-delay: 50ms; - transition-duration: 50ms; - } - - .vis-alerts-icon { - margin: 0; - padding: 0 10px; - flex: 0 0 auto; - align-self: center; - } - - .vis-alerts-text { - flex: 1 1 auto; - margin: 0; - padding: 0; - } - - .vis-alerts-close { - cursor: pointer; - } - - .vis-alert { - margin: 0 10px 10px; - padding: 5px 10px 5px 5px; - color: @alert-vis-alert-color; - border-radius: @alert-border-radius; - border: 1px solid; - border-color: @alert-vis-alert-border; - display: flex; - } - - .vis-alert-success { - .alert-variant(fade(@alert-success-bg, 75%); @alert-success-border; @alert-success-text); - } - - .vis-alert-info { - .alert-variant(fade(@alert-info-bg, 75%); @alert-info-border; @alert-info-text); - } - - .vis-alert-warning { - .alert-variant(fade(@alert-warning-bg, 75%); @alert-warning-border; @alert-warning-text); - } - - .vis-alert-danger { - .alert-variant(fade(@alert-danger-bg, 75%); @alert-danger-border; @alert-danger-text); - } diff --git a/src/ui/public/vislib/styles/_error.less b/src/ui/public/vislib/styles/_error.less deleted file mode 100644 index cac25a20142eb0a..000000000000000 --- a/src/ui/public/vislib/styles/_error.less +++ /dev/null @@ -1,10 +0,0 @@ -.error { - flex: 1 1 100%; - text-align: center; - - p { - margin-top: 15%; - font-size: 18px; - text-wrap: wrap; - } -} diff --git a/src/ui/public/vislib/styles/_layout.less b/src/ui/public/vislib/styles/_layout.less deleted file mode 100644 index 3b4bd639746a91a..000000000000000 --- a/src/ui/public/vislib/styles/_layout.less +++ /dev/null @@ -1,272 +0,0 @@ -.vislib-container { - flex: 1 1 0; /* 1 */ - display: flex; - flex-direction: row; - overflow: auto; - - &.vislib-container--legend-left { - flex-direction: row-reverse; - } - &.vislib-container--legend-right { - flex-direction: row; - } - &.vislib-container--legend-top { - flex-direction: column-reverse; - } - &.vislib-container--legend-bottom { - flex-direction: column; - } -} - -.vislib-chart { - display: flex; - flex: 1 1 auto; - min-height: 0; - min-width: 0; -} - -.visualize-chart { - display: flex; - flex: 1 1 auto; - min-height: 0; - min-width: 0; -} - -.vis-wrapper { - display: flex; - flex: 1 1 100%; - flex-direction: row; - min-height: 0; - min-width: 0; - overflow: hidden; - padding: 10px 0; -} - -.vis-wrapper svg { - overflow: visible; -} - -/* SVG Element Default Styling */ -.vis-wrapper { - rect { - opacity: 1; - - &:hover { - opacity: @vis-hover-opacity; - } - } - - circle { - opacity: 0; - - &:hover { - opacity: 1; - stroke-width: 10px; - stroke-opacity: @vis-hover-opacity; - } - } -} - -/* YAxis logic */ -.y-axis-col-wrapper { - display: flex; - flex-direction: column; - min-height: 0; - min-width: 0; -} - -.y-axis-col { - display: flex; - flex-direction: row; - flex: 1 0 36px; - min-height: 0; - min-width: 0; -} - -.y-axis-spacer-block { - min-height: 0px; -} - -.y-axis-div-wrapper { - display: flex; - flex-direction: column; - min-height: 20px; - min-width: 0; -} - -.y-axis-div { - flex: 1 1 25px; - min-width: 1px; - min-height: 14px; - margin: 5px 0px; -} - -.y-axis-title { - min-height: 14px; - min-width: 1px; -} - -.y-axis-chart-title { - display: flex; - flex-direction: column; - min-height: 14px; - min-width: 0; -} - -.y-axis-title text, .x-axis-title text { - font-size: 9pt; - fill: @vis-axis-title-color; - font-weight: bold; -} - -.chart-title { - flex: 1 1 100%; - min-height: 14px; - min-width: 14px; -} - -.chart-title text { - font-size: 11px; - fill: @vis-chart-title-color; -} - -.vis-col-wrapper { - display: flex; - flex: 1 0 0px; - flex-direction: column; - min-height: 0; - min-width: 0; -} - -.chart-wrapper { - display: flex; - flex: 1 0 0px; - overflow: visible; - margin: 5px; - min-height: 0; - min-width: 0; -} - -.chart-wrapper-row .chart-wrapper { - margin-left: 0px; - margin-right: 0px; -} - -.chart-wrapper-column .chart-wrapper { - margin-top: 0px; - margin-bottom: 0px; -} - -.chart-wrapper-column { - display: flex; - flex: 1 0 20px; - flex-direction: row; - min-height: 0; - min-width: 0; -} - -.chart-wrapper-row { - display: flex; - flex-direction: column; - flex: 1 1 100%; - min-height: 0; - min-width: 0; -} - -.chart { - flex: 1 1 100%; - min-height: 0; - min-width: 0; - - > svg { - display: block; - } - - overflow: visible; -} - -.chart-row { - flex: 1 1 auto; - min-height: 0; - min-width: 0; -} - -.chart-column { - flex: 1 1 auto; - min-height: 0; - min-width: 0; -} - -.x-axis-wrapper { - display: flex; - flex-direction: column; - min-height: 0px; - min-width: 0; - overflow: visible; -} - -.x-axis-div-wrapper { - display: flex; - flex-direction: row; - min-height: 0px; - min-width: 0; -} - -.x-axis-chart-title { - display: flex; - flex-direction: row; - min-height: 1px; - max-height: 15px; - min-width: 20px; -} - -.x-axis-title { - min-width: 20px; -} - -.x-axis-div { - min-height: 0px; - min-width: 1px; - margin: 0px 5px; - width: 100%; -} - -.x-axis-div svg { - float: left; /* for some reason svg wont get positioned in top left corner of container div without this */ -} - -.axis-wrapper-top .axis-div svg { - margin-bottom: -5px; -} - -.chart-first { - margin-top: 0px; - margin-left: 0px -} -.chart-last { - margin-bottom: 0px; - margin-right: 0px; -} - -.label-line { - opacity: .3; - stroke: black; - stroke-width: 2px; - fill: none; -} - -.label-text { - font-size: 130%; - font-weight: normal; -} - -// SASSTODO: Make sure these colors convert to theme variables -.tab-dashboard.theme-dark { - .y-axis-title text, .x-axis-title text { - fill: @gray10; - } - - .chart-title text { - fill: @gray10; - } -} diff --git a/src/ui/public/vislib/styles/_legend.less b/src/ui/public/vislib/styles/_legend.less deleted file mode 100644 index 92a5965452e9a2b..000000000000000 --- a/src/ui/public/vislib/styles/_legend.less +++ /dev/null @@ -1,152 +0,0 @@ -@import "~ui/styles/mixins"; -@import "~ui/styles/variables"; - -visualize-legend { - display: flex; - flex-direction: row; -} - -.legend-collapse-button { - align-self: flex-start; -} - -.legend-col-wrapper { - .flex-parent(0, 0, auto); - z-index: 10; - min-height: 0; - overflow: hidden; - flex-direction: row; - padding-top: 5px; - - .vislib-container--legend-left & { - flex-direction: row-reverse; - } - .vislib-container--legend-right & { - flex-direction: row; - } - .vislib-container--legend-top & { - flex-direction: column-reverse; - width: 100%; - padding-left: 25px; - } - .vislib-container--legend-bottom & { - flex-direction: column; - width: 100%; - padding-left: 25px; - } - - .header { - cursor: pointer; - width: 15px; - } - - .legend-ul { - width: 150px; - flex: 1 1 auto; - overflow-x: hidden; - overflow-y: auto; - color: @legend-item-color; - list-style-type: none; - padding: 0; - margin-bottom: 0; - visibility: visible; - min-height: 0; - font-size: 12px; - line-height: 13px; - text-align: left; - - flex-direction: column; - - .vislib-container--legend-top &, - .vislib-container--legend-bottom & { - width: auto; - overflow-y: hidden; - - .legend-value { - display: inline-block; - } - } - } - - .legend-ul.hidden { - visibility: hidden; - } -} - -.legend-value { - &:hover { - cursor: pointer; - } -} - - .legend-value-title { - padding: 3px; - - &:hover { - background-color: @sidebar-hover-bg; - } - } - - .legend-value-truncate { - overflow-x: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .legend-value-full { - white-space: normal; - word-break: break-all; - background-color: @sidebar-hover-bg; - } - - .legend-value-details { - border-bottom: 1px solid @sidebar-bg; - } - - .legend-value-color-picker { - width: 130px; - margin: auto; - - .dot { - line-height: 14px; - margin: 2px; - font-size: 14px; - } - - .dot:hover { - margin: 0px; - font-size: 18px - } - } - -// SASSTODO: Make sure these colors convert to theme variables -.tab-dashboard.theme-dark { - .legend { - background-color: #272727; - } - - .legend-col-wrapper { - - .legend-ul { - border-left-color: @gray7; - color: @gray10; - } - - } - - .legend-value-title { - padding: 3px; - - &:hover { - background-color: darken(@gray7, 5%); - } - } - - .legend-value-full { - background-color: @gray3; - } - - .legend-value-details { - border-bottom: 1px solid @gray7; - } -} diff --git a/src/ui/public/vislib/styles/_svg.less b/src/ui/public/vislib/styles/_svg.less deleted file mode 100644 index 41a2fa8c9c78f4a..000000000000000 --- a/src/ui/public/vislib/styles/_svg.less +++ /dev/null @@ -1,114 +0,0 @@ -@import (reference) "~ui/styles/variables"; - -/* Axis Styles */ -.axis { - shape-rendering: crispEdges; - stroke-width: 1px; - - line, path { - stroke: @svg-axis-color; - fill: none; - shape-rendering: crispEdges; - } -} - -.tick text { - font-size: 8pt; - fill: @svg-tick-text-color; -} - -.chart-text { - fill: @svg-tick-text-color; -} - -.label-line { - stroke: @svg-tick-text-color; -} - -.label-text { - fill: @svg-tick-text-color; -} - -/* Brush Styling */ -.brush .extent { - stroke: @svg-brush-color; - fill-opacity: .125; - shape-rendering: crispEdges; -} - -@vis-hover-opacity: 0.8; - - -.overlap_area { - opacity: 0.8; -} - -.series > path, -.series > rect { - fill-opacity: 0.8; - stroke-opacity: 1; - stroke-width: 0; -} - -.blur_shape { - opacity: 0.3 !important; -} - - -.slice { - stroke: white; - stroke-width: 2px; - &:hover { - opacity: @vis-hover-opacity; - } -} - -.leaflet-clickable { - &:hover { - stroke-width: 10px; - stroke-opacity: @vis-hover-opacity; - } -} - -/* Visualization Styling */ -.line { - circle { - opacity: 1; - - &:hover { - stroke-width: 10px; - stroke-opacity: @vis-hover-opacity; - } - } -} - -.endzone { - opacity: 0.05; - fill: @svg-endzone-bg; - pointer-events: none; -} - -// SASSTODO: Make sure these colors convert to theme variables -.tab-dashboard.theme-dark { - .axis { - line, path { - stroke: @gray8; - } - } - - .tick text { - fill: @gray10; - } - - .chart-label { - fill: @gray10; - } - - .brush .extent { - stroke: @white; - } - - .endzone { - fill: @white; - } -} diff --git a/src/ui/public/vislib/styles/_tooltip.less b/src/ui/public/vislib/styles/_tooltip.less deleted file mode 100644 index 059f18cd0c2b515..000000000000000 --- a/src/ui/public/vislib/styles/_tooltip.less +++ /dev/null @@ -1,82 +0,0 @@ -@import (reference) "~ui/styles/variables"; - -.vis-tooltip, -.vis-tooltip-sizing-clone { - visibility: hidden; - line-height: 1.1; - font-size: @font-size-base; - font-weight: normal; - background: @tooltip-bg !important; - color: @tooltip-color !important; - border-radius: 4px; - position: fixed; - z-index: 120; - word-wrap: break-word; - max-width: 40%; - overflow: hidden; - pointer-events: none; - - > :last-child { - margin-bottom: @tooltip-space; - } - - > * { - margin: @tooltip-space @tooltip-space 0; - } - - .bsTooltip-label { - font-family: @font-family-sans-serif; - color: tint(@globalColorBlue, 40%); - font-weight: normal; - - th { - font-weight: @tooltip-bold; - } - } - - .bsTooltip-value { - font-family: @font-family-sans-serif; - } - - table { - td,th { - padding: @tooltip-space-tight; - - &.row-bucket { - word-break: break-all; - } - } - } -} - -.vis-tooltip-header { - margin: 0 0 @tooltip-space 0; - padding: @tooltip-space-tight @tooltip-space; - display: flex; - align-items: center; - line-height: 1.5; - - &:last-child { - margin-bottom: 0; - } - - + * { - margin-top: @tooltip-space; - } -} - - .vis-tooltip-header-icon { - flex: 0 0 auto; - padding-right: @tooltip-space; - } - - .vis-tooltip-header-text { - flex: 1 1 200px; - } - -.vis-tooltip-sizing-clone { - visibility: hidden; - position: fixed; - top: -500px; - left: -500px; -} diff --git a/src/ui/public/vislib/styles/main.less b/src/ui/public/vislib/styles/main.less deleted file mode 100644 index 3c1c25ab908feed..000000000000000 --- a/src/ui/public/vislib/styles/main.less +++ /dev/null @@ -1,8 +0,0 @@ -@import (reference) "~ui/styles/variables"; - -@import "./_error"; -@import "./_layout"; -@import "./_legend"; -@import "./_svg"; -@import "./_tooltip"; -@import "./_alerts"; diff --git a/src/ui/public/vislib/vis.js b/src/ui/public/vislib/vis.js index b64bdc93f9bbf90..311c32965899416 100644 --- a/src/ui/public/vislib/vis.js +++ b/src/ui/public/vislib/vis.js @@ -21,7 +21,6 @@ import _ from 'lodash'; import d3 from 'd3'; import { KbnError } from '../errors'; import { EventsProvider } from '../events'; -import './styles/main.less'; import { VislibVisConfigProvider } from './lib/vis_config'; import { VisHandlerProvider } from './lib/handler'; @@ -105,7 +104,7 @@ export function VislibVisProvider(Private) { * @method destroy */ destroy() { - const selection = d3.select(this.el).select('.vis-wrapper'); + const selection = d3.select(this.el).select('.visWrapper'); if (this.handler) this._runOnHandler('destroy'); diff --git a/src/ui/public/vislib/vislib.js b/src/ui/public/vislib/vislib.js index f8b6faa841a2dc4..ce53fb598dafb2d 100644 --- a/src/ui/public/vislib/vislib.js +++ b/src/ui/public/vislib/vislib.js @@ -23,7 +23,6 @@ import './lib/types'; import './lib/layout/layout_types'; import './lib/data'; import './visualizations/vis_types'; -import './styles/main.less'; import { VislibVisProvider } from './vis'; // prefetched for faster optimization runs diff --git a/src/ui/public/vislib/visualizations/point_series/area_chart.js b/src/ui/public/vislib/visualizations/point_series/area_chart.js index a8b3bd1bf1a79a2..bbc13c2ff8b8f75 100644 --- a/src/ui/public/vislib/visualizations/point_series/area_chart.js +++ b/src/ui/public/vislib/visualizations/point_series/area_chart.js @@ -100,7 +100,7 @@ export function VislibVisualizationsAreaChartProvider(Private) { .attr('data-label', data.label) .style('fill', () => color(data.label)) .style('stroke', () => color(data.label)) - .classed('overlap_area', function () { + .classed('visAreaChart__overlapArea', function () { return isOverlapping; }) .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); diff --git a/src/ui/public/visualize/_index.scss b/src/ui/public/visualize/_index.scss new file mode 100644 index 000000000000000..192091fb04e3c9a --- /dev/null +++ b/src/ui/public/visualize/_index.scss @@ -0,0 +1 @@ +@import './components/index'; diff --git a/src/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap b/src/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap index 332d75e727a2294..e499f1c84104643 100644 --- a/src/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap @@ -2,7 +2,7 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = `
      ', () => { const wrapper = mount(); jest.runAllTimers(); await renderPromise; - expect(wrapper.find('.visualize-chart').text()).toMatch(/markdown/); + expect(wrapper.find('.visChart').text()).toMatch(/markdown/); }); it('should re-render on param change', async () => { @@ -96,7 +96,7 @@ describe('', () => { jest.runAllTimers(); await renderPromise; - expect(wrapper.find('.visualize-chart').text()).toBe('new text'); + expect(wrapper.find('.visChart').text()).toBe('new text'); expect(renderComplete).toHaveBeenCalledTimes(2); }); }); diff --git a/src/ui/public/visualize/components/visualization_chart.tsx b/src/ui/public/visualize/components/visualization_chart.tsx index 91587fcc945fc30..a2c63cd08de633e 100644 --- a/src/ui/public/visualize/components/visualization_chart.tsx +++ b/src/ui/public/visualize/components/visualization_chart.tsx @@ -87,13 +87,13 @@ class VisualizationChart extends React.Component { public render() { return ( -
      +
      {this.props.vis.type.title} visualization, not yet accessible
      diff --git a/src/ui/public/visualize/components/visualization_noresults.tsx b/src/ui/public/visualize/components/visualization_noresults.tsx index 74194a5a291b0f3..48628e99cfcff72 100644 --- a/src/ui/public/visualize/components/visualization_noresults.tsx +++ b/src/ui/public/visualize/components/visualization_noresults.tsx @@ -29,7 +29,7 @@ export class VisualizationNoResults extends React.Component +
{{detail.label}} + {{detail.label}} {{detail.value}} ({{detail.percent}})
- - - - - - - - - - - - - - - -
- Below is a table of - 0 - items. -
- - - - - - - - - -
-
- - No items found - -
-
-
-
-
-
-
-
- -
-
-
-
+ + opbeans-python + + `; -exports[`ErrorGroupOverview -> List should render with data 1`] = ` -.c0 { - font-size: 16px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} +exports[`ErrorGroupOverview -> List should render empty state 1`] = ` + +`; -
-
-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Below is a table of - 2 - items. -
- - - - - - - - - -
- - -
- nodejs -
-
-
- N/A -
-
-
- 0 tpm -
-
-
- 46.1 err. -
-
- - -
- python -
-
-
- 92 ms -
-
-
- 86.9 tpm -
-
-
- 12.6 err. -
-
-
-
-
-
-
-
- -
-
-
-
+exports[`ErrorGroupOverview -> List should render with data 1`] = ` + `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx similarity index 65% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 76317a51fa6f2e7..5ac5530540466f0 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -4,16 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { RelativeLink } from '../../../../utils/url'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { fontSizes, truncate } from '../../../../style/variables'; -import TooltipOverlay from '../../../shared/TooltipOverlay'; -import { asMillis, asDecimal } from '../../../../utils/formatters'; +import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { RelativeLink } from '../../../../utils/url'; import { ManagedTable } from '../../../shared/ManagedTable'; -function formatNumber(value) { +interface Props { + items: IServiceListItem[]; + noItemsMessage?: React.ReactNode; +} + +function formatNumber(value: number) { if (value === 0) { return '0'; } else if (value <= 0.1) { @@ -23,7 +28,7 @@ function formatNumber(value) { } } -function formatString(value) { +function formatString(value?: string | null) { return value || 'N/A'; } @@ -32,50 +37,50 @@ const AppLink = styled(RelativeLink)` ${truncate('100%')}; `; -const SERVICE_COLUMNS = [ +export const SERVICE_COLUMNS = [ { field: 'serviceName', name: 'Name', width: '50%', sortable: true, - render: serviceName => ( - + render: (serviceName: string) => ( + {formatString(serviceName)} - + ) }, { field: 'agentName', name: 'Agent', sortable: true, - render: agentName => formatString(agentName) + render: (agentName: string) => formatString(agentName) }, { field: 'avgResponseTime', name: 'Avg. response time', sortable: true, dataType: 'number', - render: value => asMillis(value) + render: (value: number) => asMillis(value) }, { field: 'transactionsPerMinute', name: 'Trans. per minute', sortable: true, dataType: 'number', - render: value => `${formatNumber(value)} tpm` + render: (value: number) => `${formatNumber(value)} tpm` }, { field: 'errorsPerMinute', name: 'Errors per minute', sortable: true, dataType: 'number', - render: value => `${formatNumber(value)} err.` + render: (value: number) => `${formatNumber(value)} err.` } ]; -export function ServiceList({ items, noItemsMessage }) { +export function ServiceList({ items = [], noItemsMessage }: Props) { return ( ); } - -ServiceList.propTypes = { - noItemsMessage: PropTypes.node, - items: PropTypes.array -}; - -ServiceList.defaultProps = { - items: [] -}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js index 8ef92b829be9cd6..2f93eb0b2f1f0e7 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js @@ -13,11 +13,21 @@ import * as apmRestServices from '../../../../services/rest/apm'; jest.mock('../../../../services/rest/apm'); describe('Service Overview -> View', () => { + let mockAgentStatus; let wrapper; let instance; beforeEach(() => { - wrapper = shallow(); + mockAgentStatus = { + dataFound: true + }; + + // eslint-disable-next-line import/namespace + apmRestServices.loadAgentStatus = jest.fn(() => + Promise.resolve(mockAgentStatus) + ); + + wrapper = shallow(); instance = wrapper.instance(); }); @@ -40,53 +50,10 @@ describe('Service Overview -> View', () => { expect(List.props).toMatchSnapshot(); }); - describe('checking for historical data', () => { - let mockAgentStatus; - - beforeEach(() => { - mockAgentStatus = { - dataFound: true - }; - // eslint-disable-next-line import/namespace - apmRestServices.loadAgentStatus = jest.fn(() => - Promise.resolve(mockAgentStatus) - ); - }); - - it('should happen if service list status is success and data is empty', async () => { - const props = { - serviceList: { - status: STATUS.SUCCESS, - data: [] - } - }; - await instance.checkForHistoricalData(props); - expect(apmRestServices.loadAgentStatus).toHaveBeenCalledTimes(1); - }); - - it('should not happen if sevice list status is not success', async () => { - const props = { - serviceList: { - status: STATUS.FAILURE, - data: [] - } - }; - await instance.checkForHistoricalData(props); - expect(apmRestServices.loadAgentStatus).not.toHaveBeenCalled(); - }); - - it('should not happen if service list data is not empty', async () => { - const props = { - serviceList: { - status: STATUS.SUCCESS, - data: [1, 2, 3] - } - }; - await instance.checkForHistoricalData(props); - expect(apmRestServices.loadAgentStatus).not.toHaveBeenCalled(); - }); + it('should check for historical data once', () => {}); - it('should leave historical data state as true if data is found', async () => { + describe('checking for historical data', () => { + it('should set historical data to true if data is found', async () => { const props = { serviceList: { status: STATUS.SUCCESS, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/view.tsx similarity index 51% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/view.js rename to x-pack/plugins/apm/public/components/app/ServiceOverview/view.tsx index 9e0759bb735a12b..2bd9373b94c3c61 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/view.tsx @@ -4,40 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiSpacer } from '@elastic/eui'; import React, { Component } from 'react'; -import { STATUS } from '../../../constants'; -import { isEmpty } from 'lodash'; +import { RRRRenderResponse } from 'react-redux-request'; +import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { loadAgentStatus } from '../../../services/rest/apm'; -import { ServiceList } from './ServiceList'; -import { EuiSpacer } from '@elastic/eui'; import { ServiceListRequest } from '../../../store/reactReduxRequest/serviceList'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink'; +import { ServiceList } from './ServiceList'; -export class ServiceOverview extends Component { - state = { - historicalDataFound: true - }; +interface Props { + urlParams: IUrlParams; + serviceList: RRRRenderResponse; +} - async checkForHistoricalData({ serviceList }) { - if (serviceList.status === STATUS.SUCCESS && isEmpty(serviceList.data)) { - const result = await loadAgentStatus(); - if (!result.dataFound) { - this.setState({ historicalDataFound: false }); - } - } - } +interface State { + historicalDataFound: boolean; +} + +export class ServiceOverview extends Component { + public state = { historicalDataFound: true }; - componentDidMount() { - this.checkForHistoricalData(this.props); + public async checkForHistoricalData() { + const result = await loadAgentStatus(); + this.setState({ historicalDataFound: result.dataFound }); } - componentDidUpdate() { - // QUESTION: Do we want to check on ANY update, or only if serviceList status/data have changed? - this.checkForHistoricalData(this.props); + public componentDidMount() { + this.checkForHistoricalData(); } - render() { + public render() { const { urlParams } = this.props; const { historicalDataFound } = this.state; @@ -54,13 +53,19 @@ export class ServiceOverview extends Component { /> ); + // Render method here uses this.props.serviceList instead of received "data" from RRR + // to make it easier to test -- mapStateToProps uses the RRR selector so the data + // is the same either way return (
( - + render={() => ( + )} />
diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx index 3d14db033bd4183..b0c8cba46705555 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx @@ -10,7 +10,7 @@ import { RRRRenderResponse } from 'react-redux-request'; import { ITransactionGroup } from '../../../../typings/TransactionGroup'; // @ts-ignore import { TraceListRequest } from '../../../store/reactReduxRequest/traceList'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; import { TraceList } from './TraceList'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index f7321df13066078..90439a4194179f5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -14,7 +14,7 @@ import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; import { fromQuery, history, toQuery } from '../../../../utils/url'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; -import EmptyMessage from '../../../shared/EmptyMessage'; +import { EmptyMessage } from '../../../shared/EmptyMessage'; interface IChartPoint { sample?: IBucket['sample']; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx index b917db2c25685c8..08b068de1a5a590 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -20,14 +20,39 @@ import { } from '../../../../../../style/variables'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; -interface ItemBarProps { - type: 'transaction' | 'span'; +type ItemType = 'transaction' | 'span'; + +interface IContainerStyleProps { + type: ItemType; + timelineMargins: ITimelineMargins; + isSelected: boolean; +} + +interface IBarStyleProps { + type: ItemType; left: number; width: number; color: string; } -const ItemBar = styled('div')` +const Container = styled('div')` + position: relative; + display: block; + user-select: none; + padding-top: ${px(units.half)}; + padding-bottom: ${props => + px(props.type === 'span' ? units.plus + units.quarter : units.plus)}; + margin-right: ${props => px(props.timelineMargins.right)}; + margin-left: ${props => px(props.timelineMargins.left)}; + border-top: 1px solid ${colors.gray4}; + background-color: ${props => (props.isSelected ? colors.gray5 : 'initial')}; + cursor: pointer; + &:hover { + background-color: ${colors.gray5}; + } +`; + +const ItemBar = styled('div')` box-sizing: border-box; position: relative; height: ${px(unit)}; @@ -35,44 +60,30 @@ const ItemBar = styled('div')` background-color: ${props => props.color}; `; -// Note: "direction: rtl;" is here to prevent text from running off of -// the right edge and instead pushing it to the left. For an example of -// how this works, see here: https://codepen.io/sqren/pen/JrXNjY -const SpanLabel = styled.div` +const ItemLabel = styled.div` white-space: nowrap; - position: relative; - direction: rtl; + position: absolute; + right: 0; + width: auto; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; text-align: left; - margin: ${px(units.quarter)} 0 0; + margin: 0; +`; + +const SpanLabel = styled(ItemLabel)` + font-weight: normal; font-family: ${fontFamilyCode}; font-size: ${fontSizes.small}; + bottom: ${px(units.half)}; `; -const TransactionLabel = styled(SpanLabel)` +const TransactionLabel = styled(ItemLabel)` font-weight: 600; font-family: ${fontFamily}; font-size: ${fontSize}; -`; - -interface IContainerProps { - item: IWaterfallItem; - timelineMargins: ITimelineMargins; - isSelected: boolean; -} - -const Container = styled('div')` - position: relative; - display: block; - user-select: none; - padding: ${px(units.half)} ${props => px(props.timelineMargins.right)} - ${props => px(props.item.docType === 'span' ? units.half : units.quarter)} - ${props => px(props.timelineMargins.left)}; - border-top: 1px solid ${colors.gray4}; - background-color: ${props => (props.isSelected ? colors.gray5 : 'initial')}; - cursor: pointer; - &:hover { - background-color: ${colors.gray5}; - } + bottom: ${px(units.quarter)}; `; interface ITimelineMargins { @@ -117,29 +128,24 @@ export function WaterfallItem({ const width = (item.duration / totalDuration) * 100; const left = ((item.offset + item.skew) / totalDuration) * 100; - const Label = item.docType === 'transaction' ? TransactionLabel : SpanLabel; + const Label = item.docType === 'span' ? SpanLabel : TransactionLabel; - // Note: the appears *after* the item name in the DOM order - // because this label is styled with "direction: rtl;" so that the name - // itself doesn't flow outside the box to the right. return ( - - ); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts index 295682df068eca7..609d7f5477e5be5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts @@ -8,7 +8,7 @@ import { Transaction } from 'x-pack/plugins/apm/typings/Transaction'; import { getAgentMarks } from './get_agent_marks'; describe('getAgentMarks', () => { - it('should sort the marks', () => { + it('should sort the marks by time', () => { const transaction: Transaction = { transaction: { marks: { @@ -33,4 +33,11 @@ describe('getAgentMarks', () => { } as any; expect(getAgentMarks(transaction)).toEqual([]); }); + + it('should return empty array if agent marks are missing', () => { + const transaction: Transaction = { + transaction: { marks: {} } + } as any; + expect(getAgentMarks(transaction)).toEqual([]); + }); }); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts index 0aeb1d48b945655..b0968ed7a1af80c 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts @@ -13,7 +13,7 @@ export interface AgentMark { } export function getAgentMarks(transaction: Transaction): AgentMark[] { - if (!transaction.transaction.marks) { + if (!(transaction.transaction.marks && transaction.transaction.marks.agent)) { return []; } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx index 97dcc67da090bca..2f7c5a64a37cdf5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx @@ -15,7 +15,7 @@ import { WaterfallRequest } from '../../../store/reactReduxRequest/waterfall'; import { IUrlParams } from '../../../store/urlParams'; // @ts-ignore import TransactionCharts from '../../shared/charts/TransactionCharts'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; // @ts-ignore import { KueryBar } from '../../shared/KueryBar'; // @ts-ignore diff --git a/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx index 274d063555d146b..33a0c012c16b8ca 100644 --- a/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx +++ b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx @@ -4,14 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui'; import React from 'react'; -function EmptyMessage({ +interface Props { + heading?: string; + subheading?: EuiEmptyPromptProps['body']; + hideSubheading?: boolean; +} + +const EmptyMessage: React.SFC = ({ heading = 'No data found.', subheading = 'Try another time range or reset the search filter.', hideSubheading = false -}) { +}) => { return ( ); -} +}; -// tslint:disable-next-line:no-default-export -export default EmptyMessage; +export { EmptyMessage }; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js index 0bec4149914d4dd..b7a7f5f567f7bc8 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js @@ -10,7 +10,7 @@ import { isEmpty, get } from 'lodash'; import CodePreview from '../../shared/CodePreview'; import { Ellipsis } from '../../shared/Icons'; import { units, px } from '../../../style/variables'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; import { EuiLink, EuiTitle } from '@elastic/eui'; const LibraryFrameToggle = styled.div` diff --git a/x-pack/plugins/apm/public/services/rest/apm.ts b/x-pack/plugins/apm/public/services/rest/apm.ts index 3a390492f8c7212..d065be4fa28a287 100644 --- a/x-pack/plugins/apm/public/services/rest/apm.ts +++ b/x-pack/plugins/apm/public/services/rest/apm.ts @@ -7,7 +7,7 @@ // @ts-ignore import { camelizeKeys } from 'humps'; import { ServiceResponse } from 'x-pack/plugins/apm/server/lib/services/get_service'; -import { ServiceListItemResponse } from 'x-pack/plugins/apm/server/lib/services/get_services'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { IDistributionResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution'; import { Span } from 'x-pack/plugins/apm/typings/Span'; import { Transaction } from 'x-pack/plugins/apm/typings/Transaction'; @@ -58,7 +58,7 @@ export async function loadServiceList({ start, end, kuery -}: IUrlParams): Promise { +}: IUrlParams): Promise { return callApi({ pathname: `/api/apm/services`, query: { diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.js b/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.tsx similarity index 59% rename from x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.js rename to x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.tsx index 10175d3c507bb83..ef5e59649b7674c 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.js +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.tsx @@ -5,19 +5,31 @@ */ import React from 'react'; +import { Request, RRRRender, RRRRenderResponse } from 'react-redux-request'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { loadServiceList } from '../../services/rest/apm'; -import { Request } from 'react-redux-request'; +import { IReduxState } from '../rootReducer'; +import { IUrlParams } from '../urlParams'; +// @ts-ignore import { createInitialDataSelector } from './helpers'; const ID = 'serviceList'; -const INITIAL_DATA = []; +const INITIAL_DATA: IServiceListItem[] = []; const withInitialData = createInitialDataSelector(INITIAL_DATA); -export function getServiceList(state) { +export function getServiceList( + state: IReduxState +): RRRRenderResponse { return withInitialData(state.reactReduxRequest[ID]); } -export function ServiceListRequest({ urlParams, render }) { +export function ServiceListRequest({ + urlParams, + render +}: { + urlParams: IUrlParams; + render: RRRRender; +}) { const { start, end, kuery } = urlParams; if (!(start && end)) { diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts new file mode 100644 index 000000000000000..9da683b48b618d7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { + AgentName, + APM_TELEMETRY_DOC_ID, + ApmTelemetry, + createApmTelementry, + getSavedObjectsClient, + storeApmTelemetry +} from '../apm_telemetry'; + +describe('apm_telemetry', () => { + describe('createApmTelementry', () => { + it('should create a ApmTelemetry object with boolean flag and frequency map of the given list of AgentNames', () => { + const apmTelemetry = createApmTelementry([ + AgentName.GoLang, + AgentName.NodeJs, + AgentName.GoLang, + AgentName.JsBase + ]); + expect(apmTelemetry.has_any_services).toBe(true); + expect(apmTelemetry.services_per_agent).toMatchObject({ + [AgentName.GoLang]: 2, + [AgentName.NodeJs]: 1, + [AgentName.JsBase]: 1 + }); + }); + it('should ignore undefined or unknown AgentName values', () => { + const apmTelemetry = createApmTelementry([ + AgentName.GoLang, + AgentName.NodeJs, + AgentName.GoLang, + AgentName.JsBase, + 'example-platform' as any, + undefined as any + ]); + expect(apmTelemetry.services_per_agent).toMatchObject({ + [AgentName.GoLang]: 2, + [AgentName.NodeJs]: 1, + [AgentName.JsBase]: 1 + }); + }); + }); + + describe('storeApmTelemetry', () => { + let server: any; + let apmTelemetry: ApmTelemetry; + let savedObjectsClientInstance: any; + + beforeEach(() => { + savedObjectsClientInstance = { create: jest.fn() }; + const callWithInternalUser = jest.fn(); + const internalRepository = jest.fn(); + server = { + savedObjects: { + SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), + getSavedObjectsRepository: jest.fn(() => internalRepository) + }, + plugins: { + elasticsearch: { + getCluster: jest.fn(() => ({ callWithInternalUser })) + } + } + }; + apmTelemetry = { + has_any_services: true, + services_per_agent: { + [AgentName.GoLang]: 2, + [AgentName.NodeJs]: 1, + [AgentName.JsBase]: 1 + } + }; + }); + + it('should call savedObjectsClient create with the given ApmTelemetry object', () => { + storeApmTelemetry(server, apmTelemetry); + expect(savedObjectsClientInstance.create.mock.calls[0][1]).toBe( + apmTelemetry + ); + }); + + it('should call savedObjectsClient create with the apm-telemetry document type and ID', () => { + storeApmTelemetry(server, apmTelemetry); + expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe( + 'apm-telemetry' + ); + expect(savedObjectsClientInstance.create.mock.calls[0][2].id).toBe( + APM_TELEMETRY_DOC_ID + ); + }); + + it('should call savedObjectsClient create with overwrite: true', () => { + storeApmTelemetry(server, apmTelemetry); + expect(savedObjectsClientInstance.create.mock.calls[0][2].overwrite).toBe( + true + ); + }); + }); + + describe('getSavedObjectsClient', () => { + let server: any; + let savedObjectsClientInstance: any; + let callWithInternalUser: any; + let internalRepository: any; + + beforeEach(() => { + savedObjectsClientInstance = { create: jest.fn() }; + callWithInternalUser = jest.fn(); + internalRepository = jest.fn(); + server = { + savedObjects: { + SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), + getSavedObjectsRepository: jest.fn(() => internalRepository) + }, + plugins: { + elasticsearch: { + getCluster: jest.fn(() => ({ callWithInternalUser })) + } + } + }; + }); + + it('should use internal user "admin"', () => { + getSavedObjectsClient(server); + + expect(server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith( + 'admin' + ); + }); + + it('should call getSavedObjectsRepository with a cluster using the internal user context', () => { + getSavedObjectsClient(server); + + expect( + server.savedObjects.getSavedObjectsRepository + ).toHaveBeenCalledWith(callWithInternalUser); + }); + + it('should return a SavedObjectsClient initialized with the saved objects internal repository', () => { + const result = getSavedObjectsClient(server); + + expect(result).toBe(savedObjectsClientInstance); + expect(server.savedObjects.SavedObjectsClient).toHaveBeenCalledWith( + internalRepository + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts new file mode 100644 index 000000000000000..f136030dd565224 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import { countBy } from 'lodash'; + +// Support telemetry for additional agent types by appending definitions in +// mappings.json and the AgentName enum. + +export enum AgentName { + Python = 'python', + Java = 'java', + NodeJs = 'nodejs', + JsBase = 'js-base', + Ruby = 'ruby', + GoLang = 'go' +} + +export interface ApmTelemetry { + has_any_services: boolean; + services_per_agent: { [agentName in AgentName]?: number }; +} + +export const APM_TELEMETRY_DOC_ID = 'apm-telemetry'; + +export function createApmTelementry( + agentNames: AgentName[] = [] +): ApmTelemetry { + const validAgentNames = agentNames.filter(agentName => + Object.values(AgentName).includes(agentName) + ); + return { + has_any_services: validAgentNames.length > 0, + services_per_agent: countBy(validAgentNames) + }; +} + +export function storeApmTelemetry( + server: Server, + apmTelemetry: ApmTelemetry +): void { + const savedObjectsClient = getSavedObjectsClient(server); + savedObjectsClient.create('apm-telemetry', apmTelemetry, { + id: APM_TELEMETRY_DOC_ID, + overwrite: true + }); +} + +export function getSavedObjectsClient(server: Server): any { + const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster( + 'admin' + ); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + return new SavedObjectsClient(internalRepository); +} diff --git a/x-pack/plugins/infra/server/graphql/capabilities/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts similarity index 54% rename from x-pack/plugins/infra/server/graphql/capabilities/index.ts rename to x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index 3f6f9541eda335d..96325952fb1a796 100644 --- a/x-pack/plugins/infra/server/graphql/capabilities/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -4,5 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createCapabilitiesResolvers } from './resolvers'; -export { capabilitiesSchema } from './schema.gql'; +export { + ApmTelemetry, + AgentName, + storeApmTelemetry, + createApmTelementry, + APM_TELEMETRY_DOC_ID +} from './apm_telemetry'; +export { makeApmUsageCollector } from './make_apm_usage_collector'; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/make_apm_usage_collector.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/make_apm_usage_collector.ts new file mode 100644 index 000000000000000..d09ea4cdab51ec7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/make_apm_usage_collector.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import { + APM_TELEMETRY_DOC_ID, + ApmTelemetry, + createApmTelementry, + getSavedObjectsClient +} from './apm_telemetry'; + +// TODO this type should be defined by the platform +interface KibanaHapiServer extends Server { + usage: { + collectorSet: { + makeUsageCollector: any; + register: any; + }; + }; +} + +export function makeApmUsageCollector(server: KibanaHapiServer): void { + const apmUsageCollector = server.usage.collectorSet.makeUsageCollector({ + type: 'apm', + fetch: async (): Promise => { + const savedObjectsClient = getSavedObjectsClient(server); + try { + const apmTelemetrySavedObject = await savedObjectsClient.get( + 'apm-telemetry', + APM_TELEMETRY_DOC_ID + ); + return apmTelemetrySavedObject.attributes; + } catch (err) { + return createApmTelementry(); + } + } + }); + server.usage.collectorSet.register(apmUsageCollector); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_services.ts b/x-pack/plugins/apm/server/lib/services/get_services.ts index eeb39c256e8a32e..6d2019e64c1db27 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services.ts @@ -14,17 +14,15 @@ import { } from '../../../common/constants'; import { Setup } from '../helpers/setup_request'; -export interface ServiceListItemResponse { - service_name: string; - agent_name: string | undefined; - transactions_per_minute: number; - errors_per_minute: number; - avg_response_time: number; +export interface IServiceListItem { + serviceName: string; + agentName: string | undefined; + transactionsPerMinute: number; + errorsPerMinute: number; + avgResponseTime: number; } -export async function getServices( - setup: Setup -): Promise { +export async function getServices(setup: Setup): Promise { const { start, end, esFilterQuery, client, config } = setup; const params = { @@ -118,11 +116,11 @@ export async function getServices( const errorsPerMinute = totalErrors / deltaAsMinutes; return { - service_name: bucket.key, - agent_name: oc(bucket).agents.buckets[0].key(), - transactions_per_minute: transactionsPerMinute, - errors_per_minute: errorsPerMinute, - avg_response_time: bucket.avg.value + serviceName: bucket.key, + agentName: oc(bucket).agents.buckets[0].key(), + transactionsPerMinute, + errorsPerMinute, + avgResponseTime: bucket.avg.value }; }); } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/__test__/get_anomaly_aggs.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/__test__/get_anomaly_aggs.test.ts new file mode 100644 index 000000000000000..7ded075a7ae6551 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/__test__/get_anomaly_aggs.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { getAnomalyAggs } from '../get_anomaly_aggs'; + +test('getAnomalyAggs should swallow HTTP errors', () => { + const httpError = new Error('anomaly lookup failed') as any; + httpError.statusCode = 418; + const failClient = jest.fn(() => Promise.reject(httpError)); + + return expect(getAnomalyAggs({ client: failClient })).resolves.toEqual(null); +}); + +test('getAnomalyAggs should throw other errors', () => { + const otherError = new Error('anomaly lookup ASPLODED') as any; + const failClient = jest.fn(() => Promise.reject(otherError)); + + return expect( + getAnomalyAggs({ + client: failClient + }) + ).rejects.toThrow(otherError); +}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs.js b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs.js index 86fb84fd8887172..cae78df06f725fa 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs.js +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs.js @@ -62,10 +62,13 @@ export async function getAnomalyAggs({ try { const resp = await client('search', params); return resp.aggregations; - } catch (e) { - if (e.statusCode === 404) { + } catch (err) { + if ('statusCode' in err) { + // swallow HTTP errors because there are lots of reasons + // the ml index lookup may fail, and we're ok with that return null; } - throw e; + + throw err; } } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_avg_response_time_anomalies.js b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_avg_response_time_anomalies.js index c6d411eb91884f5..3e950d919b6cc9b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_avg_response_time_anomalies.js +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_avg_response_time_anomalies.js @@ -34,7 +34,7 @@ export async function getAvgResponseTimeAnomalies({ if (!aggs) { return { - message: 'ml index does not exist' + message: 'Error reading machine learning index' }; } diff --git a/x-pack/plugins/apm/server/routes/__test__/routeFailures.test.ts b/x-pack/plugins/apm/server/routes/__test__/routeFailures.test.ts new file mode 100644 index 000000000000000..44a82dd25f25ccd --- /dev/null +++ b/x-pack/plugins/apm/server/routes/__test__/routeFailures.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +// @ts-ignore +import { initErrorsApi } from '../errors'; +import { initServicesApi } from '../services'; +// @ts-ignore +import { initStatusApi } from '../status_check'; +import { initTracesApi } from '../traces'; +import { initTransactionsApi } from '../transactions'; + +describe('route handlers fail properly', () => { + let consoleErrorSpy: any; + + async function testRouteFailures(init: (server: Server) => void) { + const mockServer = { route: jest.fn() }; + init((mockServer as unknown) as Server); + expect(mockServer.route).toHaveBeenCalled(); + + const routes = mockServer.route.mock.calls; + const mockReq = { + params: {}, + query: {}, + pre: { + setup: { + config: { get: jest.fn() }, + client: jest.fn(() => Promise.reject(new Error('request failed'))) + } + } + }; + + routes.forEach(async (route, i) => { + test(`route ${i + 1} of ${ + routes.length + } should fail with a Boom error`, async () => { + await expect(route[0].handler(mockReq)).rejects.toMatchObject({ + message: 'request failed', + isBoom: true + }); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + }); + }); + } + + beforeEach(() => { + consoleErrorSpy = jest + .spyOn(global.console, 'error') + .mockImplementation(undefined); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + describe('error routes', async () => { + await testRouteFailures(initErrorsApi); + }); + + describe('service routes', async () => { + await testRouteFailures(initServicesApi); + }); + + describe('status check routes', async () => { + await testRouteFailures(initStatusApi); + }); + + describe('trace routes', async () => { + await testRouteFailures(initTracesApi); + }); + + describe('transaction routes', async () => { + await testRouteFailures(initTransactionsApi); + }); +}); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 31cf772c5e5dc35..ef7bbf6c86c8640 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -6,6 +6,11 @@ import Boom from 'boom'; import { Server } from 'hapi'; +import { + AgentName, + createApmTelementry, + storeApmTelemetry +} from '../lib/apm_telemetry'; import { withDefaultValidators } from '../lib/helpers/input_validation'; import { setupRequest } from '../lib/helpers/setup_request'; import { getService } from '../lib/services/get_service'; @@ -17,7 +22,7 @@ const defaultErrorHandler = (err: Error) => { // tslint:disable-next-line console.error(err.stack); // @ts-ignore - return Boom.boomify(err, { statusCode: 400 }); + throw Boom.boomify(err, { statusCode: 400 }); }; export function initServicesApi(server: Server) { @@ -30,9 +35,23 @@ export function initServicesApi(server: Server) { query: withDefaultValidators() } }, - handler: req => { + handler: async req => { const { setup } = req.pre; - return getServices(setup).catch(defaultErrorHandler); + + let serviceBucketList; + try { + serviceBucketList = await getServices(setup); + } catch (error) { + return defaultErrorHandler(error); + } + + // Store telemetry data derived from serviceBucketList + const apmTelemetry = createApmTelementry( + serviceBucketList.map(({ agentName }) => agentName as AgentName) + ); + storeApmTelemetry(server, apmTelemetry); + + return serviceBucketList; } }); diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 56588130ba3dda5..7c291cf78512854 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -17,7 +17,7 @@ const defaultErrorHandler = (err: Error) => { // tslint:disable-next-line console.error(err.stack); // @ts-ignore - return Boom.boomify(err, { statusCode: 400 }); + throw Boom.boomify(err, { statusCode: 400 }); }; export function initTracesApi(server: Server) { diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 2774e090e1d5718..01311f896ef8df1 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -22,7 +22,7 @@ const defaultErrorHandler = (err: Error) => { // tslint:disable-next-line console.error(err.stack); // @ts-ignore - Boom.boomify(err, { statusCode: err.statusCode || 400 }); + throw Boom.boomify(err, { statusCode: 400 }); }; export function initTransactionsApi(server: Server) { diff --git a/x-pack/plugins/apm/typings/Transaction.ts b/x-pack/plugins/apm/typings/Transaction.ts index 21422f0c3f0aef6..88ddcccff7de32a 100644 --- a/x-pack/plugins/apm/typings/Transaction.ts +++ b/x-pack/plugins/apm/typings/Transaction.ts @@ -37,7 +37,7 @@ interface Context { } interface Marks { - agent: { + agent?: { [name: string]: number; }; } diff --git a/x-pack/plugins/beats_management/public/index.tsx b/x-pack/plugins/beats_management/public/index.tsx index 749fd8cecf9c6b7..fcd261151282bca 100644 --- a/x-pack/plugins/beats_management/public/index.tsx +++ b/x-pack/plugins/beats_management/public/index.tsx @@ -14,7 +14,7 @@ import { FrontendLibs } from './lib/lib'; import { PageRouter } from './router'; function startApp(libs: FrontendLibs) { - libs.framework.registerManagementSection('beats', 'Central Management', BASE_PATH); + libs.framework.registerManagementSection('beats', 'Central Management (Beta)', BASE_PATH); libs.framework.render( diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx index e5334a622317dc4..25ffe06f449496a 100644 --- a/x-pack/plugins/beats_management/public/pages/main/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -117,7 +117,7 @@ class MainPagesComponent extends React.PureComponent diff --git a/x-pack/plugins/canvas/__tests__/fixtures/function_specs.js b/x-pack/plugins/canvas/__tests__/fixtures/function_specs.js index 433b5ec64b753fa..6ca97f159a1d595 100644 --- a/x-pack/plugins/canvas/__tests__/fixtures/function_specs.js +++ b/x-pack/plugins/canvas/__tests__/fixtures/function_specs.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Fn } from '../../common/lib/fn'; +import { Fn } from '@kbn/interpreter/common/lib/fn'; import { functions as browserFns } from '../../canvas_plugin_src/functions/browser'; import { functions as commonFns } from '../../canvas_plugin_src/functions/common'; import { functions as serverFns } from '../../canvas_plugin_src/functions/server/src'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.js index c85cc9e0d5bafa9..fcda50653380cc0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getType } from '../../../common/lib/get_type'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; export const asFn = () => ({ name: 'as', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/clog.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/clog.js deleted file mode 100644 index db4cc4179762fce..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/clog.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const clog = () => ({ - name: 'clog', - help: 'Outputs the context to the console', - fn: context => { - console.log(context); //eslint-disable-line no-console - return context; - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.js index 410dbc60db95281..f20c78bb1fa07c0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.js @@ -11,7 +11,6 @@ import { asFn } from './as'; import { axisConfig } from './axisConfig'; import { compare } from './compare'; import { containerStyle } from './containerStyle'; -import { clog } from './clog'; import { context } from './context'; import { columns } from './columns'; import { csv } from './csv'; @@ -65,7 +64,6 @@ export const functions = [ any, asFn, axisConfig, - clog, columns, compare, containerStyle, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.js index db5578020529604..c02c6a2d2691ba2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getType } from '../../../common/lib/get_type'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; export const mapColumn = () => ({ name: 'mapColumn', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_flot_axis_config.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_flot_axis_config.js index 1a8ee7daf737048..ce4b9170d1710b3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_flot_axis_config.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_flot_axis_config.js @@ -5,7 +5,7 @@ */ import { get, map } from 'lodash'; -import { getType } from '../../../../common/lib/get_type'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; export const getFlotAxisConfig = (axis, argValue, { columns, ticks, font } = {}) => { if (!argValue || argValue.show === false) return { show: false }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.js index 77580be49719a58..b144cf179652d05 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getType } from '../../../common/lib/get_type'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; export const staticColumn = () => ({ name: 'staticColumn', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/index.js index 863fa41a90be51f..04f92bc47525629 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/index.js @@ -7,7 +7,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { get } from 'lodash'; -import { fromExpression, toExpression } from '../../../common/lib/ast'; +import { fromExpression, toExpression } from '@kbn/interpreter/common/lib/ast'; import { DropdownFilter } from './component'; export const dropdownFilter = () => ({ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter/time_filter.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter/time_filter.js index d04a8f0b54d9667..7ace73bc430d51a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter/time_filter.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter/time_filter.js @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { get } from 'lodash'; -import { fromExpression } from '../../../../../common/lib/ast'; +import { fromExpression } from '@kbn/interpreter/common/lib/ast'; import { TimePicker } from '../time_picker'; import { TimePickerMini } from '../time_picker_mini'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js index cdc4f563e1340bb..2ffc7bcf44208b7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js @@ -7,7 +7,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { get, set } from 'lodash'; -import { fromExpression, toExpression } from '../../../common/lib/ast'; +import { fromExpression, toExpression } from '@kbn/interpreter/common/lib/ast'; import { TimeFilter } from './components/time_filter'; export const timeFilter = () => ({ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/boolean.js b/x-pack/plugins/canvas/canvas_plugin_src/types/boolean.js deleted file mode 100644 index 697277a471feaef..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/boolean.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const boolean = () => ({ - name: 'boolean', - from: { - null: () => false, - number: n => Boolean(n), - string: s => Boolean(s), - }, - to: { - render: value => { - const text = `${value}`; - return { - type: 'render', - as: 'text', - value: { text }, - }; - }, - datatable: value => ({ - type: 'datatable', - columns: [{ name: 'value', type: 'boolean' }], - rows: [{ value }], - }), - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/error.js b/x-pack/plugins/canvas/canvas_plugin_src/types/error.js deleted file mode 100644 index 51051c804db5606..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/error.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const error = () => ({ - name: 'error', - to: { - render: input => { - const { error, info } = input; - return { - type: 'render', - as: 'error', - value: { - error, - info, - }, - }; - }, - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/filter.js b/x-pack/plugins/canvas/canvas_plugin_src/types/filter.js deleted file mode 100644 index 8627dd20bb89f9b..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/filter.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const filter = () => ({ - name: 'filter', - from: { - null: () => { - return { - type: 'filter', - // Any meta data you wish to pass along. - meta: {}, - // And filters. If you need an "or", create a filter type for it. - and: [], - }; - }, - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/image.js b/x-pack/plugins/canvas/canvas_plugin_src/types/image.js deleted file mode 100644 index f63d3f1b8b2aa52..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/image.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const image = () => ({ - name: 'image', - to: { - render: input => { - return { - type: 'render', - as: 'image', - value: input, - }; - }, - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/index.js b/x-pack/plugins/canvas/canvas_plugin_src/types/index.js deleted file mode 100644 index 2e9a4fa02ef8ee4..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/index.js +++ /dev/null @@ -1,33 +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 { boolean } from './boolean'; -import { datatable } from './datatable'; -import { error } from './error'; -import { filter } from './filter'; -import { image } from './image'; -import { nullType } from './null'; -import { number } from './number'; -import { pointseries } from './pointseries'; -import { render } from './render'; -import { shape } from './shape'; -import { string } from './string'; -import { style } from './style'; - -export const typeSpecs = [ - boolean, - datatable, - error, - filter, - image, - number, - nullType, - pointseries, - render, - shape, - string, - style, -]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/number.js b/x-pack/plugins/canvas/canvas_plugin_src/types/number.js deleted file mode 100644 index 63ee587075fdd85..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/number.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const number = () => ({ - name: 'number', - from: { - null: () => 0, - boolean: b => Number(b), - string: n => Number(n), - }, - to: { - render: value => { - const text = `${value}`; - return { - type: 'render', - as: 'text', - value: { text }, - }; - }, - datatable: value => ({ - type: 'datatable', - columns: [{ name: 'value', type: 'number' }], - rows: [{ value }], - }), - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/pointseries.js b/x-pack/plugins/canvas/canvas_plugin_src/types/pointseries.js deleted file mode 100644 index 1a0073862005016..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/pointseries.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const pointseries = () => ({ - name: 'pointseries', - from: { - null: () => { - return { - type: 'pointseries', - rows: [], - columns: [], - }; - }, - }, - to: { - render: (pointseries, types) => { - const datatable = types.datatable.from(pointseries, types); - return { - type: 'render', - as: 'table', - value: { - datatable, - showHeader: true, - }, - }; - }, - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/register.js b/x-pack/plugins/canvas/canvas_plugin_src/types/register.js deleted file mode 100644 index e960dd0f6566ae1..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/register.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'babel-polyfill'; -import { typeSpecs } from './index'; - -typeSpecs.forEach(canvas.register); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/render.js b/x-pack/plugins/canvas/canvas_plugin_src/types/render.js deleted file mode 100644 index 0f261f0398816f7..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/render.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const render = () => ({ - name: 'render', - from: { - '*': v => ({ - type: 'render', - as: 'debug', - value: v, - }), - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/shape.js b/x-pack/plugins/canvas/canvas_plugin_src/types/shape.js deleted file mode 100644 index 1b306b7b1c39117..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/shape.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const shape = () => ({ - name: 'shape', - to: { - render: input => { - return { - type: 'render', - as: 'shape', - value: input, - }; - }, - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/string.js b/x-pack/plugins/canvas/canvas_plugin_src/types/string.js deleted file mode 100644 index c8d58aaaffbcad5..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/string.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const string = () => ({ - name: 'string', - from: { - null: () => '', - boolean: b => String(b), - number: n => String(n), - }, - to: { - render: text => { - return { - type: 'render', - as: 'text', - value: { text }, - }; - }, - datatable: value => ({ - type: 'datatable', - columns: [{ name: 'value', type: 'string' }], - rows: [{ value }], - }), - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/style.js b/x-pack/plugins/canvas/canvas_plugin_src/types/style.js deleted file mode 100644 index 62632c03231ad73..000000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/style.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const style = () => ({ - name: 'style', - from: { - null: () => { - return { - type: 'style', - spec: {}, - css: '', - }; - }, - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js index 0540a146034605b..64a426a84a7cd58 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js @@ -9,8 +9,8 @@ import { compose, withPropsOnChange, withHandlers } from 'recompose'; import PropTypes from 'prop-types'; import { EuiSelect, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { sortBy } from 'lodash'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; import { createStatefulPropHoc } from '../../../../public/components/enhance/stateful_prop'; -import { getType } from '../../../../common/lib/get_type'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { SimpleMathFunction } from './simple_math_function'; import { getFormObject } from './get_form_object'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js index d5540d050227f98..5027dbbf3f566fe 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js @@ -16,10 +16,7 @@ import { EuiButton, EuiFieldText, } from '@elastic/eui'; - -// TODO: (clintandrewhall) This is a quick fix for #25342 -- we should figure out how to use the overall component. -import { Loading } from '../../../../public/components/loading/loading'; - +import { Loading } from '../../../../public/components/loading'; import { FileUpload } from '../../../../public/components/file_upload'; import { elasticOutline } from '../../../lib/elastic_outline'; import { resolveFromArgs } from '../../../../common/lib/resolve_dataurl'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js index e0f8e56df8b6f84..fc0f89692f646bc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js @@ -7,8 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { get } from 'lodash'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; import { PalettePicker } from '../../../public/components/palette_picker'; -import { getType } from '../../../common/lib/get_type'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; const PaletteArgInput = ({ onValueChange, argValue, renderError }) => { diff --git a/x-pack/plugins/canvas/common/functions/to.js b/x-pack/plugins/canvas/common/functions/to.js index 6f15569c27a11b2..25446b286865250 100644 --- a/x-pack/plugins/canvas/common/functions/to.js +++ b/x-pack/plugins/canvas/common/functions/to.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { castProvider } from '../interpreter/cast'; +import { castProvider } from '@kbn/interpreter/common/interpreter/cast'; export const to = () => ({ name: 'to', diff --git a/x-pack/plugins/canvas/common/interpreter/create_error.js b/x-pack/plugins/canvas/common/interpreter/create_error.js deleted file mode 100644 index 5de9819330dbd03..000000000000000 --- a/x-pack/plugins/canvas/common/interpreter/create_error.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const createError = err => ({ - type: 'error', - error: { - stack: process.env.NODE_ENV === 'production' ? undefined : err.stack, - message: typeof err === 'string' ? err : err.message, - }, -}); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/arg.js b/x-pack/plugins/canvas/common/lib/__tests__/arg.js deleted file mode 100644 index f8badc67175ace8..000000000000000 --- a/x-pack/plugins/canvas/common/lib/__tests__/arg.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { Arg } from '../arg'; - -describe('Arg', () => { - it('sets required to false by default', () => { - const isOptional = new Arg({ - name: 'optional_me', - }); - expect(isOptional.required).to.equal(false); - - const isRequired = new Arg({ - name: 'require_me', - required: true, - }); - expect(isRequired.required).to.equal(true); - }); -}); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/get_by_alias.js b/x-pack/plugins/canvas/common/lib/__tests__/get_by_alias.js deleted file mode 100644 index eaeeeade4cc593b..000000000000000 --- a/x-pack/plugins/canvas/common/lib/__tests__/get_by_alias.js +++ /dev/null @@ -1,74 +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 expect from 'expect.js'; -import { getByAlias } from '../get_by_alias'; - -describe('getByAlias', () => { - const fnsObject = { - foo: { name: 'foo', aliases: ['f'] }, - bar: { name: 'bar', aliases: ['b'] }, - }; - - const fnsArray = [{ name: 'foo', aliases: ['f'] }, { name: 'bar', aliases: ['b'] }]; - - it('returns the function by name', () => { - expect(getByAlias(fnsObject, 'foo')).to.be(fnsObject.foo); - expect(getByAlias(fnsObject, 'bar')).to.be(fnsObject.bar); - expect(getByAlias(fnsArray, 'foo')).to.be(fnsArray[0]); - expect(getByAlias(fnsArray, 'bar')).to.be(fnsArray[1]); - }); - - it('returns the function by alias', () => { - expect(getByAlias(fnsObject, 'f')).to.be(fnsObject.foo); - expect(getByAlias(fnsObject, 'b')).to.be(fnsObject.bar); - expect(getByAlias(fnsArray, 'f')).to.be(fnsArray[0]); - expect(getByAlias(fnsArray, 'b')).to.be(fnsArray[1]); - }); - - it('returns the function by case-insensitive name', () => { - expect(getByAlias(fnsObject, 'FOO')).to.be(fnsObject.foo); - expect(getByAlias(fnsObject, 'BAR')).to.be(fnsObject.bar); - expect(getByAlias(fnsArray, 'FOO')).to.be(fnsArray[0]); - expect(getByAlias(fnsArray, 'BAR')).to.be(fnsArray[1]); - }); - - it('returns the function by case-insensitive alias', () => { - expect(getByAlias(fnsObject, 'F')).to.be(fnsObject.foo); - expect(getByAlias(fnsObject, 'B')).to.be(fnsObject.bar); - expect(getByAlias(fnsArray, 'F')).to.be(fnsArray[0]); - expect(getByAlias(fnsArray, 'B')).to.be(fnsArray[1]); - }); - - it('handles empty strings', () => { - const emptyStringFnsObject = { '': { name: '' } }; - const emptyStringAliasFnsObject = { foo: { name: 'foo', aliases: [''] } }; - expect(getByAlias(emptyStringFnsObject, '')).to.be(emptyStringFnsObject['']); - expect(getByAlias(emptyStringAliasFnsObject, '')).to.be(emptyStringAliasFnsObject.foo); - - const emptyStringFnsArray = [{ name: '' }]; - const emptyStringAliasFnsArray = [{ name: 'foo', aliases: [''] }]; - expect(getByAlias(emptyStringFnsArray, '')).to.be(emptyStringFnsArray[0]); - expect(getByAlias(emptyStringAliasFnsArray, '')).to.be(emptyStringAliasFnsArray[0]); - }); - - it('handles "undefined" strings', () => { - const undefinedFnsObject = { undefined: { name: 'undefined' } }; - const undefinedAliasFnsObject = { foo: { name: 'undefined', aliases: ['undefined'] } }; - expect(getByAlias(undefinedFnsObject, 'undefined')).to.be(undefinedFnsObject.undefined); - expect(getByAlias(undefinedAliasFnsObject, 'undefined')).to.be(undefinedAliasFnsObject.foo); - - const emptyStringFnsArray = [{ name: 'undefined' }]; - const emptyStringAliasFnsArray = [{ name: 'foo', aliases: ['undefined'] }]; - expect(getByAlias(emptyStringFnsArray, 'undefined')).to.be(emptyStringFnsArray[0]); - expect(getByAlias(emptyStringAliasFnsArray, 'undefined')).to.be(emptyStringAliasFnsArray[0]); - }); - - it('returns undefined if not found', () => { - expect(getByAlias(fnsObject, 'baz')).to.be(undefined); - expect(getByAlias(fnsArray, 'baz')).to.be(undefined); - }); -}); diff --git a/x-pack/plugins/canvas/common/lib/arg.js b/x-pack/plugins/canvas/common/lib/arg.js deleted file mode 100644 index 7713fcb342bc255..000000000000000 --- a/x-pack/plugins/canvas/common/lib/arg.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { includes } from 'lodash'; - -export function Arg(config) { - if (config.name === '_') throw Error('Arg names must not be _. Use it in aliases instead.'); - this.name = config.name; - this.required = config.required || false; - this.help = config.help || ''; - this.types = config.types || []; - this.default = config.default; - this.aliases = config.aliases || []; - this.multi = config.multi == null ? false : config.multi; - this.resolve = config.resolve == null ? true : config.resolve; - this.options = config.options || []; - this.accepts = type => { - if (!this.types.length) return true; - return includes(config.types, type); - }; -} diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.js b/x-pack/plugins/canvas/common/lib/autocomplete.js index d87e199de46717c..5a18c2570919b3c 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.js +++ b/x-pack/plugins/canvas/common/lib/autocomplete.js @@ -5,8 +5,8 @@ */ import { uniq } from 'lodash'; -import { parse } from './grammar'; -import { getByAlias } from './get_by_alias'; +import { parse } from '@kbn/interpreter/common/lib/grammar'; +import { getByAlias } from '@kbn/interpreter/common/lib/get_by_alias'; const MARKER = 'CANVAS_SUGGESTION_MARKER'; diff --git a/x-pack/plugins/canvas/common/lib/functions_registry.js b/x-pack/plugins/canvas/common/lib/functions_registry.js deleted file mode 100644 index af8e8f0b122d069..000000000000000 --- a/x-pack/plugins/canvas/common/lib/functions_registry.js +++ /dev/null @@ -1,16 +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 { Registry } from '../../common/lib/registry'; -import { Fn } from '../lib/fn'; - -class FunctionsRegistry extends Registry { - wrapper(obj) { - return new Fn(obj); - } -} - -export const functionsRegistry = new FunctionsRegistry(); diff --git a/x-pack/plugins/canvas/common/lib/get_by_alias.js b/x-pack/plugins/canvas/common/lib/get_by_alias.js deleted file mode 100644 index c9986a502400865..000000000000000 --- a/x-pack/plugins/canvas/common/lib/get_by_alias.js +++ /dev/null @@ -1,20 +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. - */ - -/** - * This is used for looking up function/argument definitions. It looks through - * the given object/array for a case-insensitive match, which could be either the - * `name` itself, or something under the `aliases` property. - */ -export function getByAlias(specs, name) { - const lowerCaseName = name.toLowerCase(); - return Object.values(specs).find(({ name, aliases }) => { - if (name.toLowerCase() === lowerCaseName) return true; - return (aliases || []).some(alias => { - return alias.toLowerCase() === lowerCaseName; - }); - }); -} diff --git a/x-pack/plugins/canvas/common/lib/get_type.js b/x-pack/plugins/canvas/common/lib/get_type.js deleted file mode 100644 index 8d2b5a13cb2837b..000000000000000 --- a/x-pack/plugins/canvas/common/lib/get_type.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function getType(node) { - if (node == null) return 'null'; - if (typeof node === 'object') { - if (!node.type) throw new Error('Objects must have a type propery'); - return node.type; - } - - return typeof node; -} diff --git a/x-pack/plugins/canvas/common/lib/index.js b/x-pack/plugins/canvas/common/lib/index.js index 5d56a5026590deb..321a4abff44e0df 100644 --- a/x-pack/plugins/canvas/common/lib/index.js +++ b/x-pack/plugins/canvas/common/lib/index.js @@ -5,8 +5,6 @@ */ export * from './datatable'; -export * from './arg'; -export * from './ast'; export * from './autocomplete'; export * from './constants'; export * from './dataurl'; @@ -14,15 +12,10 @@ export * from './errors'; export * from './expression_form_handlers'; export * from './fetch'; export * from './find_in_object'; -export * from './fn'; export * from './fonts'; -export * from './functions_registry'; -export * from './get_by_alias'; export * from './get_colors_from_palette'; export * from './get_field_type'; export * from './get_legend_config'; -export * from './get_type'; -export * from './grammar'; export * from './handlebars'; export * from './hex_to_rgb'; export * from './httpurl'; @@ -30,10 +23,6 @@ export * from './latest_change'; export * from './missing_asset'; export * from './palettes'; export * from './pivot_object_array'; -export * from './registry'; export * from './resolve_dataurl'; -export * from './serialize'; -export * from './type'; -export * from './types_registry'; export * from './unquote_string'; export * from './url'; diff --git a/x-pack/plugins/canvas/common/lib/serialize.js b/x-pack/plugins/canvas/common/lib/serialize.js deleted file mode 100644 index 0786f6f06b3a357..000000000000000 --- a/x-pack/plugins/canvas/common/lib/serialize.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, identity } from 'lodash'; -import { getType } from '../lib/get_type'; - -export function serializeProvider(types) { - return { - serialize: provider('serialize'), - deserialize: provider('deserialize'), - }; - - function provider(key) { - return context => { - const type = getType(context); - const typeDef = types[type]; - const fn = get(typeDef, key) || identity; - return fn(context); - }; - } -} diff --git a/x-pack/plugins/canvas/common/lib/types_registry.js b/x-pack/plugins/canvas/common/lib/types_registry.js deleted file mode 100644 index 3d2bb65e9fa0f78..000000000000000 --- a/x-pack/plugins/canvas/common/lib/types_registry.js +++ /dev/null @@ -1,16 +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 { Registry } from '../../common/lib/registry'; -import { Type } from '../../common/lib/type'; - -class TypesRegistry extends Registry { - wrapper(obj) { - return new Type(obj); - } -} - -export const typesRegistry = new TypesRegistry(); diff --git a/x-pack/plugins/canvas/index.js b/x-pack/plugins/canvas/index.js index b92e29341a14b35..0f34eef6c2edbc8 100644 --- a/x-pack/plugins/canvas/index.js +++ b/x-pack/plugins/canvas/index.js @@ -5,9 +5,11 @@ */ import { resolve } from 'path'; +import { pathsRegistry } from '@kbn/interpreter/common/lib/paths_registry'; import init from './init'; import { mappings } from './server/mappings'; import { CANVAS_APP } from './common/lib/constants'; +import { pluginPaths } from './plugin_paths'; export function canvas(kibana) { return new kibana.Plugin({ @@ -39,6 +41,9 @@ export function canvas(kibana) { }).default(); }, + preInit: () => { + pathsRegistry.registerAll(pluginPaths); + }, init, }); } diff --git a/x-pack/plugins/canvas/init.js b/x-pack/plugins/canvas/init.js index 1ef56fac4e97c28..70a8db10d7e664a 100644 --- a/x-pack/plugins/canvas/init.js +++ b/x-pack/plugins/canvas/init.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { functionsRegistry } from '@kbn/interpreter/common/lib/functions_registry'; +import { getServerRegistries } from '@kbn/interpreter/server/server_registries'; import { routes } from './server/routes'; -import { functionsRegistry } from './common/lib'; import { commonFunctions } from './common/functions'; -import { populateServerRegistries } from './server/lib/server_registries'; import { registerCanvasUsageCollector } from './server/usage'; import { loadSampleData } from './server/sample_data'; @@ -34,6 +34,6 @@ export default async function(server /*options*/) { loadSampleData(server); // Do not initialize the app until the registries are populated - await populateServerRegistries(['serverFunctions', 'types']); + await getServerRegistries(); routes(server); } diff --git a/x-pack/plugins/canvas/plugin_paths.js b/x-pack/plugins/canvas/plugin_paths.js new file mode 100644 index 000000000000000..9c9f5d1c49bdec2 --- /dev/null +++ b/x-pack/plugins/canvas/plugin_paths.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; + +export const pluginPaths = { + serverFunctions: resolve(__dirname, 'canvas_plugin/functions/server'), + browserFunctions: resolve(__dirname, 'canvas_plugin/functions/browser'), + commonFunctions: resolve(__dirname, 'canvas_plugin/functions/common'), + elements: resolve(__dirname, 'canvas_plugin/elements'), + renderers: resolve(__dirname, 'canvas_plugin/renderers'), + interfaces: resolve(__dirname, 'canvas_plugin/interfaces'), + transformUIs: resolve(__dirname, 'canvas_plugin/uis/transforms'), + datasourceUIs: resolve(__dirname, 'canvas_plugin/uis/datasources'), + modelUIs: resolve(__dirname, 'canvas_plugin/uis/models'), + viewUIs: resolve(__dirname, 'canvas_plugin/uis/views'), + argumentUIs: resolve(__dirname, 'canvas_plugin/uis/arguments'), +}; diff --git a/x-pack/plugins/canvas/public/components/app/index.js b/x-pack/plugins/canvas/public/components/app/index.js index 5f633169604d6f2..b776bf59efc994d 100644 --- a/x-pack/plugins/canvas/public/components/app/index.js +++ b/x-pack/plugins/canvas/public/components/app/index.js @@ -4,15 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createSocket } from '@kbn/interpreter/public/socket'; +import { initialize as initializeInterpreter } from '@kbn/interpreter/public/interpreter'; import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import { createSocket } from '../../socket'; -import { initialize as initializeInterpreter } from '../../lib/interpreter'; -import { populateBrowserRegistries } from '../../lib/browser_registries'; +import { populateBrowserRegistries } from '@kbn/interpreter/public/browser_registries'; import { getAppReady, getBasePath } from '../../state/selectors/app'; import { appReady, appError } from '../../state/actions/app'; -import { trackRouteChange } from './track_route_change'; +import { loadPrivateBrowserFunctions } from '../../lib/load_private_browser_functions'; +import { elementsRegistry } from '../../lib/elements_registry'; +import { renderFunctionsRegistry } from '../../lib/render_functions_registry'; +import { + argTypeRegistry, + datasourceRegistry, + modelRegistry, + transformRegistry, + viewRegistry, +} from '../../expression_types'; import { App as Component } from './app'; +import { trackRouteChange } from './track_route_change'; const mapStateToProps = state => { // appReady could be an error object @@ -24,13 +34,24 @@ const mapStateToProps = state => { }; }; +const types = { + elements: elementsRegistry, + renderers: renderFunctionsRegistry, + transformUIs: transformRegistry, + datasourceUIs: datasourceRegistry, + modelUIs: modelRegistry, + viewUIs: viewRegistry, + argumentUIs: argTypeRegistry, +}; + const mapDispatchToProps = dispatch => ({ // TODO: the correct socket path should come from upstream, using the constant here is not ideal setAppReady: basePath => async () => { try { // initialize the socket and interpreter await createSocket(basePath); - await populateBrowserRegistries(); + loadPrivateBrowserFunctions(); + await populateBrowserRegistries(types); await initializeInterpreter(); // set app state to ready diff --git a/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js b/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js index 2bd779de759d940..13ecfe89bb922c0 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js +++ b/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js @@ -8,8 +8,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { compose, withProps, withPropsOnChange } from 'recompose'; import { EuiForm, EuiTextArea, EuiButton, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { fromExpression, toExpression } from '@kbn/interpreter/common/lib/ast'; import { createStatefulPropHoc } from '../../components/enhance/stateful_prop'; -import { fromExpression, toExpression } from '../../../common/lib/ast'; export const AdvancedFailureComponent = props => { const { diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js index 8b21c38a5f6f717..f1f3fb2ddae979b 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { interpretAst } from '@kbn/interpreter/public/interpreter'; import { pure, compose, lifecycle, withState, branch, renderComponent } from 'recompose'; import { PropTypes } from 'prop-types'; import { Loading } from '../../loading'; -import { interpretAst } from '../../../lib/interpreter'; import { DatasourcePreview as Component } from './datasource_preview'; export const DatasourcePreview = compose( diff --git a/x-pack/plugins/canvas/public/components/element_content/element_content.js b/x-pack/plugins/canvas/public/components/element_content/element_content.js index 8eba2aa2ba438ec..40cca930827db4d 100644 --- a/x-pack/plugins/canvas/public/components/element_content/element_content.js +++ b/x-pack/plugins/canvas/public/components/element_content/element_content.js @@ -8,7 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { pure, compose, branch, renderComponent } from 'recompose'; import Style from 'style-it'; -import { getType } from '../../../common/lib/get_type'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; import { Loading } from '../loading'; import { RenderWithFn } from '../render_with_fn'; import { ElementShareContainer } from '../element_share_container'; @@ -23,8 +23,7 @@ const branches = [ // no renderable or renderable config value, render loading branch(({ renderable, state }) => { return !state || !renderable; - }, renderComponent(Loading)), - + }, renderComponent(({ backgroundColor }) => )), // renderable is available, but no matching element is found, render invalid branch(({ renderable, renderFunction }) => { return renderable && getType(renderable) !== 'render' && !renderFunction; @@ -90,4 +89,5 @@ ElementContent.propTypes = { onComplete: PropTypes.func.isRequired, // local, not passed through }).isRequired, state: PropTypes.string, + backgroundColor: PropTypes.string, }; diff --git a/x-pack/plugins/canvas/public/components/element_content/index.js b/x-pack/plugins/canvas/public/components/element_content/index.js index 969e96a994a3bba..9a1e943df4e7fb1 100644 --- a/x-pack/plugins/canvas/public/components/element_content/index.js +++ b/x-pack/plugins/canvas/public/components/element_content/index.js @@ -5,12 +5,19 @@ */ import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; import { get } from 'lodash'; import { renderFunctionsRegistry } from '../../lib/render_functions_registry'; +import { getSelectedPage, getPageById } from '../../state/selectors/workpad'; import { ElementContent as Component } from './element_content'; +const mapStateToProps = state => ({ + backgroundColor: getPageById(state, getSelectedPage(state)).style.background, +}); + export const ElementContent = compose( + connect(mapStateToProps), withProps(({ renderable }) => ({ renderFunction: renderFunctionsRegistry.get(get(renderable, 'as')), })) diff --git a/x-pack/plugins/canvas/public/components/expression/index.js b/x-pack/plugins/canvas/public/components/expression/index.js index 18690529d4e8004..81d73959e83b81f 100644 --- a/x-pack/plugins/canvas/public/components/expression/index.js +++ b/x-pack/plugins/canvas/public/components/expression/index.js @@ -15,9 +15,9 @@ import { branch, renderComponent, } from 'recompose'; +import { fromExpression } from '@kbn/interpreter/common/lib/ast'; import { getSelectedPage, getSelectedElement } from '../../state/selectors/workpad'; import { setExpression, flushContext } from '../../state/actions/elements'; -import { fromExpression } from '../../../common/lib/ast'; import { getFunctionDefinitions } from '../../lib/function_definitions'; import { getWindow } from '../../lib/get_window'; import { ElementNotSelected } from './element_not_selected'; diff --git a/x-pack/plugins/canvas/public/components/function_form_list/index.js b/x-pack/plugins/canvas/public/components/function_form_list/index.js index 8b6702d94340fdd..84748f5bbbbb3ff 100644 --- a/x-pack/plugins/canvas/public/components/function_form_list/index.js +++ b/x-pack/plugins/canvas/public/components/function_form_list/index.js @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { interpretAst } from '@kbn/interpreter/public/interpreter'; import { compose, withProps } from 'recompose'; import { get } from 'lodash'; +import { toExpression } from '@kbn/interpreter/common/lib/ast'; import { modelRegistry, viewRegistry, transformRegistry } from '../../expression_types'; -import { interpretAst } from '../../lib/interpreter'; -import { toExpression } from '../../../common/lib/ast'; import { FunctionFormList as Component } from './function_form_list'; function normalizeContext(chain) { diff --git a/x-pack/plugins/canvas/public/components/loading/index.js b/x-pack/plugins/canvas/public/components/loading/index.js index 9216cb55d83c27c..81fedf328718431 100644 --- a/x-pack/plugins/canvas/public/components/loading/index.js +++ b/x-pack/plugins/canvas/public/components/loading/index.js @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { getSelectedPage, getPageById } from '../../state/selectors/workpad'; +import { pure } from 'recompose'; import { Loading as Component } from './loading'; -const mapStateToProps = state => ({ - backgroundColor: getPageById(state, getSelectedPage(state)).style.background, -}); - -export const Loading = connect(mapStateToProps)(Component); +export const Loading = pure(Component); diff --git a/x-pack/plugins/canvas/public/components/loading/loading.js b/x-pack/plugins/canvas/public/components/loading/loading.js index 6b108a7606e8a4d..7a4b2083d7e4c2b 100644 --- a/x-pack/plugins/canvas/public/components/loading/loading.js +++ b/x-pack/plugins/canvas/public/components/loading/loading.js @@ -41,11 +41,12 @@ export const Loading = ({ animated, text, backgroundColor }) => { Loading.propTypes = { animated: PropTypes.bool, - text: PropTypes.string, backgroundColor: PropTypes.string, + text: PropTypes.string, }; Loading.defaultProps = { animated: false, + backgroundColor: '#000000', text: '', }; diff --git a/x-pack/plugins/canvas/public/expression_types/arg_type.js b/x-pack/plugins/canvas/public/expression_types/arg_type.js index 76f29afee7185a1..a19c726e138c265 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_type.js +++ b/x-pack/plugins/canvas/public/expression_types/arg_type.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { BaseForm } from './base_form'; export class ArgType extends BaseForm { diff --git a/x-pack/plugins/canvas/public/expression_types/datasource.js b/x-pack/plugins/canvas/public/expression_types/datasource.js index 858be2b4e33dd4d..cd9a8af5f0182a1 100644 --- a/x-pack/plugins/canvas/public/expression_types/datasource.js +++ b/x-pack/plugins/canvas/public/expression_types/datasource.js @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { RenderToDom } from '../components/render_to_dom'; import { ExpressionFormHandlers } from '../../common/lib/expression_form_handlers'; import { BaseForm } from './base_form'; diff --git a/x-pack/plugins/canvas/public/expression_types/function_form.js b/x-pack/plugins/canvas/public/expression_types/function_form.js index 70da0004ab1754f..c7bc16a5b2e2b1f 100644 --- a/x-pack/plugins/canvas/public/expression_types/function_form.js +++ b/x-pack/plugins/canvas/public/expression_types/function_form.js @@ -7,7 +7,7 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; import { isPlainObject, uniq, last, compact } from 'lodash'; -import { fromExpression } from '../../common/lib/ast'; +import { fromExpression } from '@kbn/interpreter/common/lib/ast'; import { ArgAddPopover } from '../components/arg_add_popover'; import { SidebarSection } from '../components/sidebar/sidebar_section'; import { SidebarSectionTitle } from '../components/sidebar/sidebar_section_title'; diff --git a/x-pack/plugins/canvas/public/expression_types/model.js b/x-pack/plugins/canvas/public/expression_types/model.js index bae74d75589be75..7ce1126bdec55aa 100644 --- a/x-pack/plugins/canvas/public/expression_types/model.js +++ b/x-pack/plugins/canvas/public/expression_types/model.js @@ -5,7 +5,7 @@ */ import { get, pick } from 'lodash'; -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { FunctionForm } from './function_form'; const NO_NEXT_EXP = 'no next expression'; diff --git a/x-pack/plugins/canvas/public/expression_types/transform.js b/x-pack/plugins/canvas/public/expression_types/transform.js index 216e79b9c106cd1..760eae46195d652 100644 --- a/x-pack/plugins/canvas/public/expression_types/transform.js +++ b/x-pack/plugins/canvas/public/expression_types/transform.js @@ -5,7 +5,7 @@ */ import { pick } from 'lodash'; -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { FunctionForm } from './function_form'; export class Transform extends FunctionForm { diff --git a/x-pack/plugins/canvas/public/expression_types/view.js b/x-pack/plugins/canvas/public/expression_types/view.js index ee83fe3340d760f..1b7fe13d508b0af 100644 --- a/x-pack/plugins/canvas/public/expression_types/view.js +++ b/x-pack/plugins/canvas/public/expression_types/view.js @@ -5,7 +5,7 @@ */ import { pick } from 'lodash'; -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { FunctionForm } from './function_form'; export class View extends FunctionForm { diff --git a/x-pack/plugins/canvas/public/functions/filters.js b/x-pack/plugins/canvas/public/functions/filters.js index a6f8d2a63fc5e0b..3c578a93fc3b675 100644 --- a/x-pack/plugins/canvas/public/functions/filters.js +++ b/x-pack/plugins/canvas/public/functions/filters.js @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromExpression } from '../../common/lib/ast'; -import { typesRegistry } from '../../common/lib/types_registry'; +import { interpretAst } from '@kbn/interpreter/public/interpreter'; +import { fromExpression } from '@kbn/interpreter/common/lib/ast'; +import { typesRegistry } from '@kbn/interpreter/common/lib/types_registry'; import { getState } from '../state/store'; import { getGlobalFilterExpression } from '../state/selectors/workpad'; -import { interpretAst } from '../lib/interpreter'; export const filters = () => ({ name: 'filters', diff --git a/x-pack/plugins/canvas/public/lib/arg_helpers.js b/x-pack/plugins/canvas/public/lib/arg_helpers.js index e53e26b62dd1576..e1cd8b64b323feb 100644 --- a/x-pack/plugins/canvas/public/lib/arg_helpers.js +++ b/x-pack/plugins/canvas/public/lib/arg_helpers.js @@ -5,7 +5,7 @@ */ import { includes } from 'lodash'; -import { getType } from '../../common/lib/get_type'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; /* diff --git a/x-pack/plugins/canvas/public/lib/browser_registries.js b/x-pack/plugins/canvas/public/lib/browser_registries.js deleted file mode 100644 index efceec04d6dce81..000000000000000 --- a/x-pack/plugins/canvas/public/lib/browser_registries.js +++ /dev/null @@ -1,74 +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 chrome from 'ui/chrome'; -import $script from 'scriptjs'; -import { typesRegistry } from '../../common/lib/types_registry'; -import { - argTypeRegistry, - datasourceRegistry, - transformRegistry, - modelRegistry, - viewRegistry, -} from '../expression_types'; -import { elementsRegistry } from './elements_registry'; -import { renderFunctionsRegistry } from './render_functions_registry'; -import { functionsRegistry as browserFunctions } from './functions_registry'; -import { loadPrivateBrowserFunctions } from './load_private_browser_functions'; - -const registries = { - browserFunctions: browserFunctions, - commonFunctions: browserFunctions, - elements: elementsRegistry, - types: typesRegistry, - renderers: renderFunctionsRegistry, - transformUIs: transformRegistry, - datasourceUIs: datasourceRegistry, - modelUIs: modelRegistry, - viewUIs: viewRegistry, - argumentUIs: argTypeRegistry, -}; - -let resolve = null; -let called = false; - -const populatePromise = new Promise(_resolve => { - resolve = _resolve; -}); - -export const getBrowserRegistries = () => { - return populatePromise; -}; - -export const populateBrowserRegistries = () => { - if (called) throw new Error('function should only be called once per process'); - called = true; - - // loadPrivateBrowserFunctions is sync. No biggie. - loadPrivateBrowserFunctions(); - - const remainingTypes = Object.keys(registries); - const populatedTypes = {}; - - function loadType() { - const type = remainingTypes.pop(); - window.canvas = window.canvas || {}; - window.canvas.register = d => registries[type].register(d); - - // Load plugins one at a time because each needs a different loader function - // $script will only load each of these once, we so can call this as many times as we need? - const pluginPath = chrome.addBasePath(`/api/canvas/plugins?type=${type}`); - $script(pluginPath, () => { - populatedTypes[type] = registries[type]; - - if (remainingTypes.length) loadType(); - else resolve(populatedTypes); - }); - } - - if (remainingTypes.length) loadType(); - return populatePromise; -}; diff --git a/x-pack/plugins/canvas/public/lib/elements_registry.js b/x-pack/plugins/canvas/public/lib/elements_registry.js index 898fba183c9f5ce..dc3d743f498771a 100644 --- a/x-pack/plugins/canvas/public/lib/elements_registry.js +++ b/x-pack/plugins/canvas/public/lib/elements_registry.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { Element } from './element'; class ElementsRegistry extends Registry { diff --git a/x-pack/plugins/canvas/public/lib/es_service.js b/x-pack/plugins/canvas/public/lib/es_service.js index 879bf696248192d..b6d63134c6f750e 100644 --- a/x-pack/plugins/canvas/public/lib/es_service.js +++ b/x-pack/plugins/canvas/public/lib/es_service.js @@ -26,8 +26,18 @@ export const getFields = (index = '_all') => { }; export const getIndices = () => { - return fetch - .get(`${apiPath}/es_indices`) - .then(({ data: indices }) => indices) + const savedObjectsClient = chrome.getSavedObjectsClient(); + return savedObjectsClient + .find({ + type: 'index-pattern', + fields: ['title'], + search_fields: ['title'], + perPage: 1000, + }) + .then(resp => { + return resp.savedObjects.map(savedObject => { + return savedObject.attributes.title; + }); + }) .catch(err => notify.error(err, { title: `Couldn't fetch Elasticsearch indices` })); }; diff --git a/x-pack/plugins/canvas/public/lib/function_definitions.js b/x-pack/plugins/canvas/public/lib/function_definitions.js index c4bc16a4c94c30b..c0f3496dfc08358 100644 --- a/x-pack/plugins/canvas/public/lib/function_definitions.js +++ b/x-pack/plugins/canvas/public/lib/function_definitions.js @@ -5,8 +5,8 @@ */ import uniqBy from 'lodash.uniqby'; +import { getBrowserRegistries } from '@kbn/interpreter/public/browser_registries'; import { getServerFunctions } from '../state/selectors/app'; -import { getBrowserRegistries } from './browser_registries'; export async function getFunctionDefinitions(state) { const { browserFunctions } = await getBrowserRegistries(); diff --git a/x-pack/plugins/canvas/public/lib/functions_registry.js b/x-pack/plugins/canvas/public/lib/functions_registry.js index 3cc084d8ca66ed3..36f9a631f06ea67 100644 --- a/x-pack/plugins/canvas/public/lib/functions_registry.js +++ b/x-pack/plugins/canvas/public/lib/functions_registry.js @@ -5,4 +5,4 @@ */ // export the common registry here, so it's available in plugin public code -export { functionsRegistry } from '../../common/lib/functions_registry'; +export { functionsRegistry } from '@kbn/interpreter/common/lib/functions_registry'; diff --git a/x-pack/plugins/canvas/public/lib/history_provider.js b/x-pack/plugins/canvas/public/lib/history_provider.js index 9b7f907ceeedf24..65a5acc78526de4 100644 --- a/x-pack/plugins/canvas/public/lib/history_provider.js +++ b/x-pack/plugins/canvas/public/lib/history_provider.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import lzString from 'lz-string'; -import { createBrowserHistory, createMemoryHistory, parsePath, createPath } from 'history'; -import { get } from 'lodash'; -import { APP_ROUTE } from '../../common/lib/constants'; +import { createMemoryHistory, parsePath, createPath } from 'history'; +import createHashStateHistory from 'history-extra'; import { getWindow } from './get_window'; function wrapHistoryInstance(history) { @@ -132,17 +130,7 @@ const instances = new WeakMap(); const getHistoryInstance = win => { // if no window object, use memory module if (typeof win === 'undefined' || !win.history) return createMemoryHistory(); - - const basePath = chrome.getBasePath(); - const basename = `${basePath}${APP_ROUTE}#/`; - - // hacky fix for initial page load so basename matches with the hash - if (win.location.hash === '') win.history.replaceState({}, '', `${basename}`); - - // if window object, create browser instance - return createBrowserHistory({ - basename, - }); + return createHashStateHistory(); }; export const historyProvider = (win = getWindow()) => { @@ -150,11 +138,6 @@ export const historyProvider = (win = getWindow()) => { const instance = instances.get(win); if (instance) return instance; - // temporary fix for search params before the hash; remove them via location redirect - // they can't be preserved given this upstream issue https://github.com/ReactTraining/history/issues/564 - if (get(win, 'location.search', '').length > 0) - win.location = `${chrome.getBasePath()}${APP_ROUTE}${win.location.hash}`; - // create and cache wrapped history instance const historyInstance = getHistoryInstance(win); const wrappedInstance = wrapHistoryInstance(historyInstance); diff --git a/x-pack/plugins/canvas/public/lib/parse_single_function_chain.js b/x-pack/plugins/canvas/public/lib/parse_single_function_chain.js index f8eec880af624b2..696c058e34a2b0e 100644 --- a/x-pack/plugins/canvas/public/lib/parse_single_function_chain.js +++ b/x-pack/plugins/canvas/public/lib/parse_single_function_chain.js @@ -5,7 +5,7 @@ */ import { get, mapValues, map } from 'lodash'; -import { fromExpression } from '../../common/lib/ast'; +import { fromExpression } from '@kbn/interpreter/common/lib/ast'; export function parseSingleFunctionChain(filterString) { const ast = fromExpression(filterString); diff --git a/x-pack/plugins/canvas/public/lib/render_functions_registry.js b/x-pack/plugins/canvas/public/lib/render_functions_registry.js index 3d040047aeb9acf..a34ed009a33b198 100644 --- a/x-pack/plugins/canvas/public/lib/render_functions_registry.js +++ b/x-pack/plugins/canvas/public/lib/render_functions_registry.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { RenderFunction } from './render_function'; class RenderFunctionsRegistry extends Registry { diff --git a/x-pack/plugins/canvas/public/lib/run_interpreter.js b/x-pack/plugins/canvas/public/lib/run_interpreter.js index cc0d9a7544786cc..7bb898b254ec806 100644 --- a/x-pack/plugins/canvas/public/lib/run_interpreter.js +++ b/x-pack/plugins/canvas/public/lib/run_interpreter.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromExpression } from '../../common/lib/ast'; -import { getType } from '../../common/lib/get_type'; -import { interpretAst } from './interpreter'; +import { interpretAst } from '@kbn/interpreter/public/interpreter'; +import { fromExpression } from '@kbn/interpreter/common/lib/ast'; +import { getType } from '@kbn/interpreter/common/lib/get_type'; import { notify } from './notify'; /** diff --git a/x-pack/plugins/canvas/public/lib/transitions_registry.js b/x-pack/plugins/canvas/public/lib/transitions_registry.js index 8d2e421b8233c48..8ead0aa896ab79a 100644 --- a/x-pack/plugins/canvas/public/lib/transitions_registry.js +++ b/x-pack/plugins/canvas/public/lib/transitions_registry.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Registry } from '../../common/lib/registry'; +import { Registry } from '@kbn/interpreter/common/lib/registry'; import { Transition } from '../transitions/transition'; class TransitionsRegistry extends Registry { diff --git a/x-pack/plugins/canvas/public/lib/types_registry.js b/x-pack/plugins/canvas/public/lib/types_registry.js index c1f13b1ae4612c1..05b82c744c38334 100644 --- a/x-pack/plugins/canvas/public/lib/types_registry.js +++ b/x-pack/plugins/canvas/public/lib/types_registry.js @@ -5,4 +5,4 @@ */ // export the common registry here, so it's available in plugin public code -export { typesRegistry } from '../../common/lib/types_registry'; +export { typesRegistry } from '@kbn/interpreter/common/lib/types_registry'; diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index fb82de32fc0ef4a..be157d9d8085b29 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { interpretAst } from '@kbn/interpreter/public/interpreter'; import { createAction } from 'redux-actions'; import { createThunk } from 'redux-thunks'; import { set, del } from 'object-path-immutable'; import { get, pick, cloneDeep, without } from 'lodash'; +import { toExpression, safeElementFromExpression } from '@kbn/interpreter/common/lib/ast'; import { getPages, getElementById, getSelectedPageIndex } from '../selectors/workpad'; import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; -import { toExpression, safeElementFromExpression } from '../../../common/lib/ast'; import { notify } from '../../lib/notify'; import { runInterpreter } from '../../lib/run_interpreter'; -import { interpretAst } from '../../lib/interpreter'; import { selectElement } from './transient'; import * as args from './resolved_args'; diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.js b/x-pack/plugins/canvas/public/state/selectors/workpad.js index 1db0128abab073a..6d888f60c2191c0 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.js +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.js @@ -5,7 +5,7 @@ */ import { get, omit } from 'lodash'; -import { safeElementFromExpression } from '../../../common/lib/ast'; +import { safeElementFromExpression } from '@kbn/interpreter/common/lib/ast'; import { append } from '../../lib/modify_path'; import { getAssets } from './assets'; diff --git a/x-pack/plugins/canvas/server/lib/feature_check.js b/x-pack/plugins/canvas/server/lib/feature_check.js deleted file mode 100644 index e9cec0292358256..000000000000000 --- a/x-pack/plugins/canvas/server/lib/feature_check.js +++ /dev/null @@ -1,13 +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. - */ - -// TODO: replace this when we use the method exposed by security https://github.com/elastic/kibana/pull/24616 -export const isSecurityEnabled = server => { - const kibanaSecurity = server.plugins.security; - const esSecurity = server.plugins.xpack_main.info.feature('security'); - - return kibanaSecurity && esSecurity.isAvailable() && esSecurity.isEnabled(); -}; diff --git a/x-pack/plugins/canvas/server/lib/get_plugin_paths.js b/x-pack/plugins/canvas/server/lib/get_plugin_paths.js deleted file mode 100644 index 02582e5f749cc76..000000000000000 --- a/x-pack/plugins/canvas/server/lib/get_plugin_paths.js +++ /dev/null @@ -1,83 +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 path from 'path'; -import fs from 'fs'; -import { promisify } from 'util'; -import { flatten } from 'lodash'; -import { pluginPaths } from './plugin_paths'; - -const lstat = promisify(fs.lstat); -const readdir = promisify(fs.readdir); - -const canvasPluginDirectoryName = 'canvas_plugin'; - -const isDirectory = path => - lstat(path) - .then(stat => stat.isDirectory()) - .catch(() => false); - -const isDirname = (p, name) => path.basename(p) === name; - -const getKibanaPluginsPath = () => { - const basePluginPath = path.resolve(__dirname, '..', '..', '..', '..', '..'); - - // find the kibana path in dev mode - if (isDirname(basePluginPath, 'kibana')) return path.join(basePluginPath, 'plugins'); - - // find the kibana path in the build, which lives in node_modules and requires going 1 path up - const buildPluginPath = path.join(basePluginPath, '..'); - if (isDirname(basePluginPath, 'node_modules')) { - const pluginPath = path.join(buildPluginPath, 'plugins'); - return isDirectory(pluginPath) && pluginPath; - } - - return false; -}; - -// These must all exist -const paths = [ - path.resolve(__dirname, '..', '..', '..'), // Canvas core plugins - getKibanaPluginsPath(), // Kibana plugin directory -].filter(Boolean); - -export const getPluginPaths = type => { - const typePath = pluginPaths[type]; - if (!typePath) throw new Error(`Unknown type: ${type}`); - - async function findPlugins(directory) { - const isDir = await isDirectory(directory); - if (!isDir) return; - - const names = await readdir(directory); // Get names of everything in the directory - return names - .filter(name => name[0] !== '.') - .map(name => path.resolve(directory, name, canvasPluginDirectoryName, ...typePath)); - } - - return Promise.all(paths.map(findPlugins)) - .then(dirs => - dirs.reduce((list, dir) => { - if (!dir) return list; - return list.concat(dir); - }, []) - ) - .then(possibleCanvasPlugins => { - // Check how many are directories. If lstat fails it doesn't exist anyway. - return Promise.all( - // An array - possibleCanvasPlugins.map(pluginPath => isDirectory(pluginPath)) - ).then(isDirectory => possibleCanvasPlugins.filter((pluginPath, i) => isDirectory[i])); - }) - .then(canvasPluginDirectories => { - return Promise.all( - canvasPluginDirectories.map(dir => - // Get the full path of all files in the directory - readdir(dir).then(files => files.map(file => path.resolve(dir, file))) - ) - ).then(flatten); - }); -}; diff --git a/x-pack/plugins/canvas/server/lib/get_plugin_stream.js b/x-pack/plugins/canvas/server/lib/get_plugin_stream.js deleted file mode 100644 index 6a08e2beeff8ea0..000000000000000 --- a/x-pack/plugins/canvas/server/lib/get_plugin_stream.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import fs from 'fs'; -import ss from 'stream-stream'; -import { getPluginPaths } from './get_plugin_paths'; - -export const getPluginStream = type => { - const stream = ss({ - separator: '\n', - }); - - getPluginPaths(type).then(files => { - files.forEach(file => { - stream.write(fs.createReadStream(file)); - }); - stream.end(); - }); - - return stream; -}; diff --git a/x-pack/plugins/canvas/server/lib/get_request.js b/x-pack/plugins/canvas/server/lib/get_request.js deleted file mode 100644 index d55421e437fc426..000000000000000 --- a/x-pack/plugins/canvas/server/lib/get_request.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import boom from 'boom'; -import { API_ROUTE } from '../../common/lib/constants'; - -export function getRequest(server, { headers }) { - const url = `${API_ROUTE}/ping`; - - return server - .inject({ - method: 'POST', - url, - headers, - }) - .then(res => { - if (res.statusCode !== 200) { - if (process.env.NODE_ENV !== 'production') { - console.error( - new Error(`Auth request failed: [${res.statusCode}] ${res.result.message}`) - ); - } - throw boom.unauthorized('Failed to authenticate socket connection'); - } - - return res.request; - }); -} diff --git a/x-pack/plugins/canvas/server/lib/plugin_paths.js b/x-pack/plugins/canvas/server/lib/plugin_paths.js deleted file mode 100644 index cb90cc0c0f06c3a..000000000000000 --- a/x-pack/plugins/canvas/server/lib/plugin_paths.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const pluginPaths = { - serverFunctions: ['functions', 'server'], - browserFunctions: ['functions', 'browser'], - commonFunctions: ['functions', 'common'], - types: ['types'], - elements: ['elements'], - renderers: ['renderers'], - interfaces: ['interfaces'], - transformUIs: ['uis', 'transforms'], - datasourceUIs: ['uis', 'datasources'], - modelUIs: ['uis', 'models'], - viewUIs: ['uis', 'views'], - argumentUIs: ['uis', 'arguments'], -}; diff --git a/x-pack/plugins/canvas/server/lib/route_expression/server.js b/x-pack/plugins/canvas/server/lib/route_expression/server.js deleted file mode 100644 index b24e4cb7e5e4180..000000000000000 --- a/x-pack/plugins/canvas/server/lib/route_expression/server.js +++ /dev/null @@ -1,27 +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 { getServerRegistries } from '../server_registries'; -import { interpretProvider } from '../../../common/interpreter/interpret'; -import { createHandlers } from '../create_handlers'; - -export const server = async ({ onFunctionNotFound, server, request }) => { - const { serverFunctions, types } = await getServerRegistries(['serverFunctions', 'types']); - - return { - interpret: (ast, context) => { - const interpret = interpretProvider({ - types: types.toJS(), - functions: serverFunctions.toJS(), - handlers: createHandlers(request, server), - onFunctionNotFound, - }); - - return interpret(ast, context); - }, - getFunctions: () => Object.keys(serverFunctions.toJS()), - }; -}; diff --git a/x-pack/plugins/canvas/server/lib/route_expression/thread/babeled.js b/x-pack/plugins/canvas/server/lib/route_expression/thread/babeled.js deleted file mode 100644 index b7c1e83beb7c75f..000000000000000 --- a/x-pack/plugins/canvas/server/lib/route_expression/thread/babeled.js +++ /dev/null @@ -1,19 +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. - */ - -require('babel-register')({ - ignore: [ - // stolen from kibana/src/setup_node_env/babel_register/register.js - // ignore paths matching `/node_modules/{a}/{b}`, unless `a` - // is `x-pack` and `b` is not `node_modules` - /\/node_modules\/(?!x-pack\/(?!node_modules)([^\/]+))([^\/]+\/[^\/]+)/, - ], - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/node_preset')], -}); - -require('./polyfill'); -require('./worker'); diff --git a/x-pack/plugins/canvas/server/lib/route_expression/thread/polyfill.js b/x-pack/plugins/canvas/server/lib/route_expression/thread/polyfill.js deleted file mode 100644 index be4983e9a37e8f8..000000000000000 --- a/x-pack/plugins/canvas/server/lib/route_expression/thread/polyfill.js +++ /dev/null @@ -1,18 +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. - */ - -// taken from kibana/src/setup_node_env/babel_register/polyfill.js -// ... -// `babel-preset-env` looks for and rewrites the following import -// statement into a list of import statements based on the polyfills -// necessary for our target environment (the current version of node) -// but since it does that during compilation, `import 'babel-polyfill'` -// must be in a file that is loaded with `require()` AFTER `babel-register` -// is configured. -// -// This is why we have this single statement in it's own file and require -// it from ./babeled.js -import 'babel-polyfill'; diff --git a/x-pack/plugins/canvas/server/lib/server_registries.js b/x-pack/plugins/canvas/server/lib/server_registries.js deleted file mode 100644 index cff63a1138ea37c..000000000000000 --- a/x-pack/plugins/canvas/server/lib/server_registries.js +++ /dev/null @@ -1,55 +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 { typesRegistry } from '../../common/lib/types_registry'; -import { functionsRegistry as serverFunctions } from '../../common/lib/functions_registry'; -import { getPluginPaths } from './get_plugin_paths'; - -const registries = { - serverFunctions: serverFunctions, - commonFunctions: serverFunctions, - types: typesRegistry, -}; - -let resolve = null; -let called = false; - -const populatePromise = new Promise(_resolve => { - resolve = _resolve; -}); - -export const getServerRegistries = () => { - return populatePromise; -}; - -export const populateServerRegistries = types => { - if (called) throw new Error('function should only be called once per process'); - called = true; - if (!types || !types.length) throw new Error('types is required'); - - const remainingTypes = types; - const populatedTypes = {}; - - const loadType = () => { - const type = remainingTypes.pop(); - getPluginPaths(type).then(paths => { - global.canvas = global.canvas || {}; - global.canvas.register = d => registries[type].register(d); - - paths.forEach(path => { - require(path); - }); - - global.canvas = undefined; - populatedTypes[type] = registries[type]; - if (remainingTypes.length) loadType(); - else resolve(populatedTypes); - }); - }; - - if (remainingTypes.length) loadType(); - return populatePromise; -}; diff --git a/x-pack/plugins/canvas/server/routes/es_indices/get_es_indices.js b/x-pack/plugins/canvas/server/routes/es_indices/get_es_indices.js deleted file mode 100644 index 9a28d3da42409eb..000000000000000 --- a/x-pack/plugins/canvas/server/routes/es_indices/get_es_indices.js +++ /dev/null @@ -1,20 +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 { map } from 'lodash'; - -export function getESIndices(kbnIndex, elasticsearchClient) { - const config = { - index: kbnIndex, - _source: 'index-pattern.title', - q: 'type:"index-pattern"', - size: 100, - }; - - return elasticsearchClient('search', config).then(resp => { - return map(resp.hits.hits, '_source["index-pattern"].title'); - }); -} diff --git a/x-pack/plugins/canvas/server/routes/es_indices/index.js b/x-pack/plugins/canvas/server/routes/es_indices/index.js deleted file mode 100644 index 71ce1122b0c8324..000000000000000 --- a/x-pack/plugins/canvas/server/routes/es_indices/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { partial } from 'lodash'; -import { getESIndices } from './get_es_indices'; - -// TODO: Error handling, note: esErrors -// TODO: Allow filtering by pattern name -export function esIndices(server) { - const kbnIndex = server.config().get('kibana.index'); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - - server.route({ - method: 'GET', - path: '/api/canvas/es_indices', - handler: function(request) { - return getESIndices(kbnIndex, partial(callWithRequest, request)); - }, - }); -} diff --git a/x-pack/plugins/canvas/server/routes/index.js b/x-pack/plugins/canvas/server/routes/index.js index 7ca2610d67c5f8f..45f26a423fc8459 100644 --- a/x-pack/plugins/canvas/server/routes/index.js +++ b/x-pack/plugins/canvas/server/routes/index.js @@ -5,17 +5,9 @@ */ import { workpad } from './workpad'; -import { socketApi } from './socket'; -import { translate } from './translate'; import { esFields } from './es_fields'; -import { esIndices } from './es_indices'; -import { plugins } from './plugins'; export function routes(server) { workpad(server); - socketApi(server); - translate(server); esFields(server); - esIndices(server); - plugins(server); } diff --git a/x-pack/plugins/canvas/server/routes/plugins.js b/x-pack/plugins/canvas/server/routes/plugins.js deleted file mode 100644 index be94ef52ac9e4ae..000000000000000 --- a/x-pack/plugins/canvas/server/routes/plugins.js +++ /dev/null @@ -1,25 +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 { getPluginStream } from '../lib/get_plugin_stream'; -import { pluginPaths } from '../lib/plugin_paths'; - -export function plugins(server) { - server.route({ - method: 'GET', - path: '/api/canvas/plugins', - handler: function(request, h) { - const { type } = request.query; - - if (!pluginPaths[type]) return h.response({ error: 'Invalid type' }).code(400); - - return getPluginStream(type); - }, - config: { - auth: false, - }, - }); -} diff --git a/x-pack/plugins/canvas/server/routes/translate.js b/x-pack/plugins/canvas/server/routes/translate.js deleted file mode 100644 index 6125898a7dab9ec..000000000000000 --- a/x-pack/plugins/canvas/server/routes/translate.js +++ /dev/null @@ -1,34 +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 { fromExpression, toExpression } from '../../common/lib/ast'; - -export function translate(server) { - /* - Get AST from expression - */ - server.route({ - method: 'GET', - path: '/api/canvas/ast', - handler: function(request, h) { - if (!request.query.expression) - return h.response({ error: '"expression" query is required' }).code(400); - return fromExpression(request.query.expression); - }, - }); - - server.route({ - method: 'POST', - path: '/api/canvas/expression', - handler: function(request, h) { - try { - return toExpression(request.payload); - } catch (e) { - return h.response({ error: e.message }).code(400); - } - }, - }); -} diff --git a/x-pack/plugins/canvas/server/usage/collector.js b/x-pack/plugins/canvas/server/usage/collector.js index a4e73ffe85071bb..d76d023f7c7e488 100644 --- a/x-pack/plugins/canvas/server/usage/collector.js +++ b/x-pack/plugins/canvas/server/usage/collector.js @@ -5,8 +5,8 @@ */ import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash'; +import { fromExpression } from '@kbn/interpreter/common/lib/ast'; import { CANVAS_USAGE_TYPE, CANVAS_TYPE } from '../../common/lib/constants'; -import { fromExpression } from '../../common/lib/ast'; /* * @param ast: an ast that includes functions to track diff --git a/x-pack/plugins/canvas/tasks/helpers/webpack.plugins.js b/x-pack/plugins/canvas/tasks/helpers/webpack.plugins.js index c53eccd87bd9796..8b8f3601d86e9e0 100644 --- a/x-pack/plugins/canvas/tasks/helpers/webpack.plugins.js +++ b/x-pack/plugins/canvas/tasks/helpers/webpack.plugins.js @@ -21,7 +21,6 @@ module.exports = { 'uis/arguments/all': path.join(sourceDir, 'uis/arguments/register.js'), 'functions/browser/all': path.join(sourceDir, 'functions/browser/register.js'), 'functions/common/all': path.join(sourceDir, 'functions/common/register.js'), - 'types/all': path.join(sourceDir, 'types/register.js'), }, // there were problems with the node and web targets since this code is actually diff --git a/x-pack/plugins/infra/common/graphql/introspection.json b/x-pack/plugins/infra/common/graphql/introspection.json index cd3e36a84c3069c..e0a54aa142c60e0 100644 --- a/x-pack/plugins/infra/common/graphql/introspection.json +++ b/x-pack/plugins/infra/common/graphql/introspection.json @@ -111,8 +111,8 @@ "deprecationReason": null }, { - "name": "capabilitiesByNode", - "description": "A hierarchy of capabilities available on nodes", + "name": "metadataByNode", + "description": "A hierarchy of metadata entries by node", "args": [ { "name": "nodeName", @@ -141,7 +141,7 @@ "ofType": { "kind": "LIST", "name": null, - "ofType": { "kind": "OBJECT", "name": "InfraNodeCapability", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "InfraNodeMetadata", "ofType": null } } }, "isDeprecated": false, @@ -781,9 +781,8 @@ }, { "kind": "OBJECT", - "name": "InfraNodeCapability", - "description": - "One specific capability available on a node. A capability corresponds to a fileset or metricset", + "name": "InfraNodeMetadata", + "description": "One metadata entry for a node.", "fields": [ { "name": "name", diff --git a/x-pack/plugins/infra/common/graphql/types.ts b/x-pack/plugins/infra/common/graphql/types.ts index c79280bd7f43b63..a48a0135f1ddfde 100644 --- a/x-pack/plugins/infra/common/graphql/types.ts +++ b/x-pack/plugins/infra/common/graphql/types.ts @@ -17,7 +17,7 @@ export interface InfraSource { id: string /** The id of the source */; configuration: InfraSourceConfiguration /** The raw configuration of the source */; status: InfraSourceStatus /** The status of the source */; - capabilitiesByNode: (InfraNodeCapability | null)[] /** A hierarchy of capabilities available on nodes */; + metadataByNode: (InfraNodeMetadata | null)[] /** A hierarchy of metadata entries by node */; logEntriesAround: InfraLogEntryInterval /** A consecutive span of log entries surrounding a point in time */; logEntriesBetween: InfraLogEntryInterval /** A consecutive span of log entries within an interval */; logSummaryBetween: InfraLogSummaryInterval /** A consecutive span of summary buckets within an interval */; @@ -56,8 +56,8 @@ export interface InfraIndexField { searchable: boolean /** Whether the field's values can be efficiently searched for */; aggregatable: boolean /** Whether the field's values can be aggregated */; } -/** One specific capability available on a node. A capability corresponds to a fileset or metricset */ -export interface InfraNodeCapability { +/** One metadata entry for a node. */ +export interface InfraNodeMetadata { name: string; source: string; } @@ -163,7 +163,7 @@ export namespace InfraSourceResolvers { id?: IdResolver /** The id of the source */; configuration?: ConfigurationResolver /** The raw configuration of the source */; status?: StatusResolver /** The status of the source */; - capabilitiesByNode?: CapabilitiesByNodeResolver /** A hierarchy of capabilities available on nodes */; + metadataByNode?: MetadataByNodeResolver /** A hierarchy of metadata entries by node */; logEntriesAround?: LogEntriesAroundResolver /** A consecutive span of log entries surrounding a point in time */; logEntriesBetween?: LogEntriesBetweenResolver /** A consecutive span of log entries within an interval */; logSummaryBetween?: LogSummaryBetweenResolver /** A consecutive span of summary buckets within an interval */; @@ -174,11 +174,8 @@ export namespace InfraSourceResolvers { export type IdResolver = Resolver; export type ConfigurationResolver = Resolver; export type StatusResolver = Resolver; - export type CapabilitiesByNodeResolver = Resolver< - (InfraNodeCapability | null)[], - CapabilitiesByNodeArgs - >; - export interface CapabilitiesByNodeArgs { + export type MetadataByNodeResolver = Resolver<(InfraNodeMetadata | null)[], MetadataByNodeArgs>; + export interface MetadataByNodeArgs { nodeName: string; nodeType: InfraNodeType; } @@ -289,8 +286,8 @@ export namespace InfraIndexFieldResolvers { export type SearchableResolver = Resolver; export type AggregatableResolver = Resolver; } -/** One specific capability available on a node. A capability corresponds to a fileset or metricset */ -export namespace InfraNodeCapabilityResolvers { +/** One metadata entry for a node. */ +export namespace InfraNodeMetadataResolvers { export interface Resolvers { name?: NameResolver; source?: SourceResolver; @@ -493,7 +490,7 @@ export interface InfraMetricInput { export interface SourceQueryArgs { id: string /** The id of the source */; } -export interface CapabilitiesByNodeInfraSourceArgs { +export interface MetadataByNodeInfraSourceArgs { nodeName: string; nodeType: InfraNodeType; } @@ -604,7 +601,7 @@ export enum InfraOperator { /** A segment of the log entry message */ export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment; -export namespace CapabilitiesQuery { +export namespace MetadataQuery { export type Variables = { sourceId: string; nodeId: string; @@ -619,11 +616,11 @@ export namespace CapabilitiesQuery { export type Source = { __typename?: 'InfraSource'; id: string; - capabilitiesByNode: (CapabilitiesByNode | null)[]; + metadataByNode: (MetadataByNode | null)[]; }; - export type CapabilitiesByNode = { - __typename?: 'InfraNodeCapability'; + export type MetadataByNode = { + __typename?: 'InfraNodeMetadata'; name: string; source: string; }; @@ -815,6 +812,7 @@ export namespace SourceQuery { export type Source = { __typename?: 'InfraSource'; + id: string; configuration: Configuration; status: Status; }; diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index da32e65e8ffb020..7e9c7dfb53b0cc1 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { I18nProvider } from '@kbn/i18n/react'; import { createHashHistory } from 'history'; import React from 'react'; import { ApolloProvider } from 'react-apollo'; @@ -29,14 +30,16 @@ export async function startApp(libs: InfraFrontendLibs) { }); libs.framework.render( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/x-pack/plugins/infra/public/containers/capabilities/capabilities.gql_query.ts b/x-pack/plugins/infra/public/containers/metadata/metadata.gql_query.ts similarity index 65% rename from x-pack/plugins/infra/public/containers/capabilities/capabilities.gql_query.ts rename to x-pack/plugins/infra/public/containers/metadata/metadata.gql_query.ts index 53845b463c0b5b8..9844c5b7afcb8ad 100644 --- a/x-pack/plugins/infra/public/containers/capabilities/capabilities.gql_query.ts +++ b/x-pack/plugins/infra/public/containers/metadata/metadata.gql_query.ts @@ -6,11 +6,11 @@ import gql from 'graphql-tag'; -export const capabilitiesQuery = gql` - query CapabilitiesQuery($sourceId: ID!, $nodeId: String!, $nodeType: InfraNodeType!) { +export const metadataQuery = gql` + query MetadataQuery($sourceId: ID!, $nodeId: String!, $nodeType: InfraNodeType!) { source(id: $sourceId) { id - capabilitiesByNode(nodeName: $nodeId, nodeType: $nodeType) { + metadataByNode(nodeName: $nodeId, nodeType: $nodeType) { name source } diff --git a/x-pack/plugins/infra/public/containers/capabilities/with_capabilites.tsx b/x-pack/plugins/infra/public/containers/metadata/with_metadata.tsx similarity index 60% rename from x-pack/plugins/infra/public/containers/capabilities/with_capabilites.tsx rename to x-pack/plugins/infra/public/containers/metadata/with_metadata.tsx index efdcb0e70e1d870..df8f25c40879251 100644 --- a/x-pack/plugins/infra/public/containers/capabilities/with_capabilites.tsx +++ b/x-pack/plugins/infra/public/containers/metadata/with_metadata.tsx @@ -8,34 +8,34 @@ import _ from 'lodash'; import React from 'react'; import { Query } from 'react-apollo'; -import { CapabilitiesQuery, InfraNodeType } from '../../../common/graphql/types'; +import { InfraNodeType, MetadataQuery } from '../../../common/graphql/types'; import { InfraMetricLayout } from '../../pages/metrics/layouts/types'; -import { capabilitiesQuery } from './capabilities.gql_query'; +import { metadataQuery } from './metadata.gql_query'; -interface WithCapabilitiesProps { - children: (args: WithCapabilitiesArgs) => React.ReactNode; +interface WithMetadataProps { + children: (args: WithMetadataArgs) => React.ReactNode; layouts: InfraMetricLayout[]; nodeType: InfraNodeType; nodeId: string; sourceId: string; } -interface WithCapabilitiesArgs { +interface WithMetadataArgs { filteredLayouts: InfraMetricLayout[]; error?: string | undefined; loading: boolean; } -export const WithCapabilities = ({ +export const WithMetadata = ({ children, layouts, nodeType, nodeId, sourceId, -}: WithCapabilitiesProps) => { +}: WithMetadataProps) => { return ( - - query={capabilitiesQuery} + + query={metadataQuery} fetchPolicy="no-cache" variables={{ sourceId, @@ -44,8 +44,8 @@ export const WithCapabilities = ({ }} > {({ data, error, loading }) => { - const capabilities = data && data.source && data.source.capabilitiesByNode; - const filteredLayouts = getFilteredLayouts(layouts, capabilities); + const metadata = data && data.source && data.source.metadataByNode; + const filteredLayouts = getFilteredLayouts(layouts, metadata); return children({ filteredLayouts, error: error && error.message, @@ -58,31 +58,31 @@ export const WithCapabilities = ({ const getFilteredLayouts = ( layouts: InfraMetricLayout[], - capabilities: Array | undefined + metadata: Array | undefined ): InfraMetricLayout[] => { - if (!capabilities) { + if (!metadata) { return layouts; } - const metricCapabilities: Array = capabilities - .filter(cap => cap && cap.source === 'metrics') - .map(cap => cap && cap.name); + const metricMetadata: Array = metadata + .filter(data => data && data.source === 'metrics') + .map(data => data && data.name); // After filtering out sections that can't be displayed, a layout may end up empty and can be removed. const filteredLayouts = layouts - .map(layout => getFilteredLayout(layout, metricCapabilities)) + .map(layout => getFilteredLayout(layout, metricMetadata)) .filter(layout => layout.sections.length > 0); return filteredLayouts; }; const getFilteredLayout = ( layout: InfraMetricLayout, - metricCapabilities: Array + metricMetadata: Array ): InfraMetricLayout => { // A section is only displayed if at least one of its requirements is met // All others are filtered out. const filteredSections = layout.sections.filter( - section => _.intersection(section.requires, metricCapabilities).length > 0 + section => _.intersection(section.requires, metricMetadata).length > 0 ); return { ...layout, sections: filteredSections }; }; diff --git a/x-pack/plugins/infra/public/pages/404.tsx b/x-pack/plugins/infra/public/pages/404.tsx index 956bf90e8492799..d99d7339c79d75e 100644 --- a/x-pack/plugins/infra/public/pages/404.tsx +++ b/x-pack/plugins/infra/public/pages/404.tsx @@ -4,10 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; export class NotFoundPage extends React.PureComponent { public render() { - return
No content found
; + return ( +
+ +
+ ); } } diff --git a/x-pack/plugins/infra/public/pages/error.tsx b/x-pack/plugins/infra/public/pages/error.tsx index 75b158c42c5a16b..0fd62ccd7e0a144 100644 --- a/x-pack/plugins/infra/public/pages/error.tsx +++ b/x-pack/plugins/infra/public/pages/error.tsx @@ -12,6 +12,7 @@ import { EuiPageHeaderSection, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import styled from 'styled-components'; import { Header } from '../components/header'; @@ -44,13 +45,23 @@ export const ErrorPageBody: React.SFC<{ message: string }> = ({ message }) => { -

Oops!

+

+ +

-

Please click the back button and try again.

+

+ +

diff --git a/x-pack/plugins/infra/public/pages/home/index.tsx b/x-pack/plugins/infra/public/pages/home/index.tsx index 9032b30d42266ac..20d8194e77026e2 100644 --- a/x-pack/plugins/infra/public/pages/home/index.tsx +++ b/x-pack/plugins/infra/public/pages/home/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; import { HomePageContent } from './page_content'; @@ -20,37 +21,54 @@ import { WithWaffleTimeUrlState } from '../../containers/waffle/with_waffle_time import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; import { WithSource } from '../../containers/with_source'; -export class HomePage extends React.PureComponent { - public render() { - return ( - - - {({ metricIndicesExist }) => - metricIndicesExist || metricIndicesExist === null ? ( - <> - - - -
} /> - - - - ) : ( - - {({ basePath }) => ( - - )} - - ) - } - - - ); - } +interface HomePageProps { + intl: InjectedIntl; } + +export const HomePage = injectI18n( + class extends React.PureComponent { + public static displayName = 'HomePage'; + public render() { + const { intl } = this.props; + return ( + + + {({ metricIndicesExist }) => + metricIndicesExist || metricIndicesExist === null ? ( + <> + + + +
} /> + + + + ) : ( + + {({ basePath }) => ( + + )} + + ) + } + + + ); + } + } +); diff --git a/x-pack/plugins/infra/public/pages/home/toolbar.tsx b/x-pack/plugins/infra/public/pages/home/toolbar.tsx index 5abccd9f9763da9..f933fa097e8f6dc 100644 --- a/x-pack/plugins/infra/public/pages/home/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/home/toolbar.tsx @@ -5,6 +5,8 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +// import { i18n } from '@kbn/i18n'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import React from 'react'; import { AutocompleteField } from '../../components/autocomplete_field'; @@ -20,25 +22,45 @@ import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options'; import { WithWaffleTime } from '../../containers/waffle/with_waffle_time'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; -const TITLES = { - [InfraNodeType.host]: 'Hosts', - [InfraNodeType.pod]: 'Kubernetes Pods', - [InfraNodeType.container]: 'Docker Containers', +const getTitle = (nodeType: string) => { + const TITLES = { + [InfraNodeType.host as string]: ( + + ), + [InfraNodeType.pod as string]: ( + + ), + [InfraNodeType.container as string]: ( + + ), + }; + return TITLES[nodeType]; }; -export const HomeToolbar: React.SFC = () => ( +export const HomeToolbar = injectI18n(({ intl }) => ( {({ nodeType }) => ( -

{TITLES[nodeType]}

+

{getTitle(nodeType)}

)}
-

Showing the last 1 minute of data from the time period

+

+ +

@@ -71,7 +93,10 @@ export const HomeToolbar: React.SFC = () => ( loadSuggestions={loadSuggestions} onChange={setFilterQueryDraftFromKueryExpression} onSubmit={applyFilterQueryFromKueryExpression} - placeholder="Search for infrastructure data... (e.g. host.name:host-1)" + placeholder={intl.formatMessage({ + id: 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', + defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)', + })} suggestions={suggestions} value={filterQueryDraft ? filterQueryDraft.expression : ''} /> @@ -111,4 +136,4 @@ export const HomeToolbar: React.SFC = () => (
-); +)); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 821bb1126658d29..dd28030076b92d2 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import compose from 'lodash/fp/compose'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; @@ -15,31 +16,50 @@ import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_ import { WithSource } from '../../containers/with_source'; import { getTimeFromLocation } from './query_params'; -type RedirectToNodeLogsProps = RouteComponentProps<{ +type RedirectToNodeLogsType = RouteComponentProps<{ nodeName: string; nodeType: InfraNodeType; }>; -export const RedirectToNodeLogs = ({ - match: { - params: { nodeName, nodeType }, - }, - location, -}: RedirectToNodeLogsProps) => ( - - {({ configuredFields }) => { - if (!configuredFields) { - return ; - } - - const searchString = compose( - replaceLogFilterInQueryString(`${configuredFields[nodeType]}: ${nodeName}`), - replaceLogPositionInQueryString(getTimeFromLocation(location)) - )(''); - - return ; - }} - +interface RedirectToNodeLogsProps extends RedirectToNodeLogsType { + intl: InjectedIntl; +} + +export const RedirectToNodeLogs = injectI18n( + ({ + match: { + params: { nodeName, nodeType }, + }, + location, + intl, + }: RedirectToNodeLogsProps) => ( + + {({ configuredFields }) => { + if (!configuredFields) { + return ( + + ); + } + + const searchString = compose( + replaceLogFilterInQueryString(`${configuredFields[nodeType]}: ${nodeName}`), + replaceLogPositionInQueryString(getTimeFromLocation(location)) + )(''); + + return ; + }} + + ) ); export const getNodeLogsUrl = ({ diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx index 7b24cbaa52609df..810cda560071ff3 100644 --- a/x-pack/plugins/infra/public/pages/logs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/logs/logs.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; import { LogsPageContent } from './page_content'; @@ -21,40 +22,63 @@ import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; import { WithSource } from '../../containers/with_source'; -export class LogsPage extends React.Component { - public render() { - return ( - - - {({ logIndicesExist }) => - logIndicesExist || logIndicesExist === null ? ( - <> - - - - -
} - breadcrumbs={[{ text: 'Logs' }]} - /> - - - - ) : ( - - {({ basePath }) => ( - { + public static displayName = 'LogsPage'; + public render() { + const { intl } = this.props; + return ( + + + {({ logIndicesExist }) => + logIndicesExist || logIndicesExist === null ? ( + <> + + + + +
} + breadcrumbs={[ + { + text: intl.formatMessage({ + id: 'xpack.infra.logsPage.logsBreadcrumbsText', + defaultMessage: 'Logs', + }), + }, + ]} /> - )} - - ) - } - - - ); + + + + ) : ( + + {({ basePath }) => ( + + )} + + ) + } + + + ); + } } -} +); diff --git a/x-pack/plugins/infra/public/pages/logs/toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/toolbar.tsx index 7c7c7004c467412..f3e06c2779158af 100644 --- a/x-pack/plugins/infra/public/pages/logs/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/toolbar.tsx @@ -5,6 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { injectI18n } from '@kbn/i18n/react'; import React from 'react'; import { AutocompleteField } from '../../components/autocomplete_field'; @@ -20,7 +21,7 @@ import { WithLogPosition } from '../../containers/logs/with_log_position'; import { WithLogTextview } from '../../containers/logs/with_log_textview'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; -export const LogsToolbar: React.SFC = () => ( +export const LogsToolbar = injectI18n(({ intl }) => ( @@ -40,7 +41,10 @@ export const LogsToolbar: React.SFC = () => ( loadSuggestions={loadSuggestions} onChange={setFilterQueryDraftFromKueryExpression} onSubmit={applyFilterQueryFromKueryExpression} - placeholder="Search for log entries... (e.g. host.name:host-1)" + placeholder={intl.formatMessage({ + id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder', + defaultMessage: 'Search for log entries… (e.g. host.name:host-1)', + })} suggestions={suggestions} value={filterQueryDraft ? filterQueryDraft.expression : ''} /> @@ -95,4 +99,4 @@ export const LogsToolbar: React.SFC = () => ( -); +)); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 8e4dd37b84a7111..6b089722b1f8520 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -17,6 +17,7 @@ import { EuiSideNav, EuiTitle, } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import styled, { withTheme } from 'styled-components'; import { InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types'; import { AutoSizer } from '../../components/auto_sizer'; @@ -25,7 +26,7 @@ import { Header } from '../../components/header'; import { Metrics } from '../../components/metrics'; import { MetricsTimeControls } from '../../components/metrics/time_controls'; import { ColumnarPage, PageContent } from '../../components/page'; -import { WithCapabilities } from '../../containers/capabilities/with_capabilites'; +import { WithMetadata } from '../../containers/metadata/with_metadata'; import { WithMetrics } from '../../containers/metrics/with_metrics'; import { WithMetricsTime, @@ -53,162 +54,177 @@ interface Props { node: string; }; }; + intl: InjectedIntl; } -class MetricDetailPage extends React.PureComponent { - public readonly state = { - isSideNavOpenOnMobile: false, - }; +export const MetricDetail = withTheme( + injectI18n( + class extends React.PureComponent { + public static displayName = 'MetricDetailPage'; + public readonly state = { + isSideNavOpenOnMobile: false, + }; - public render() { - const nodeName = this.props.match.params.node; - const nodeType = this.props.match.params.type as InfraNodeType; - const layoutCreator = layoutCreators[nodeType]; - if (!layoutCreator) { - return ; - } - const layouts = layoutCreator(this.props.theme); - const breadcrumbs = [{ text: nodeName }]; + public render() { + const { intl } = this.props; + const nodeName = this.props.match.params.node; + const nodeType = this.props.match.params.type as InfraNodeType; + const layoutCreator = layoutCreators[nodeType]; + if (!layoutCreator) { + return ( + + ); + } + const layouts = layoutCreator(this.props.theme); + const breadcrumbs = [{ text: nodeName }]; + + return ( + +
} + breadcrumbs={breadcrumbs} + /> + + + + {({ sourceId }) => ( + + {({ + currentTimeRange, + isAutoReloading, + setRangeTime, + startMetricsAutoReload, + stopMetricsAutoReload, + }) => ( + + {({ filteredLayouts }) => { + return ( + + {({ metrics, error, loading }) => { + if (error) { + return ; + } + const sideNav = filteredLayouts.map(item => { + return { + name: item.label, + id: item.id, + items: item.sections.map(section => ({ + id: section.id as string, + name: section.label, + onClick: this.handleClick(section), + })), + }; + }); + return ( + + + + + + + + + + + + + {({ measureRef, bounds: { width = 0 } }) => { + return ( + + + + + + + +

{nodeName}

+
+
+ +
+
+
- return ( - -
} - breadcrumbs={breadcrumbs} - /> - - - - {({ sourceId }) => ( - - {({ - currentTimeRange, - isAutoReloading, - setRangeTime, - startMetricsAutoReload, - stopMetricsAutoReload, - }) => ( - - {({ filteredLayouts }) => { - return ( - - {({ metrics, error, loading }) => { - if (error) { - return ; - } - const sideNav = filteredLayouts.map(item => { - return { - name: item.label, - id: item.id, - items: item.sections.map(section => ({ - id: section.id as string, - name: section.label, - onClick: this.handleClick(section), - })), - }; - }); - return ( - - - - - - - - - - - - - {({ measureRef, bounds: { width = 0 } }) => { - return ( - - - - - - - -

{nodeName}

-
-
- + 0 && isAutoReloading + ? false + : loading + } onChangeRangeTime={setRangeTime} - startLiveStreaming={startMetricsAutoReload} - stopLiveStreaming={stopMetricsAutoReload} /> -
-
-
- - - 0 && isAutoReloading - ? false - : loading - } - onChangeRangeTime={setRangeTime} - /> - -
-
- ); - }} -
-
- ); - }} -
- ); - }} -
+ + + + ); + }} + + + ); + }} + + ); + }} + + )} +
)} - - )} -
-
- - ); - } - - private handleClick = (section: InfraMetricLayoutSection) => () => { - const id = section.linkToId || section.id; - const el = document.getElementById(id); - if (el) { - el.scrollIntoView(); - } - }; + + + + ); + } - private toggleOpenOnMobile = () => { - this.setState({ - isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile, - }); - }; -} + private handleClick = (section: InfraMetricLayoutSection) => () => { + const id = section.linkToId || section.id; + const el = document.getElementById(id); + if (el) { + el.scrollIntoView(); + } + }; -export const MetricDetail = withTheme(MetricDetailPage); + private toggleOpenOnMobile = () => { + this.setState({ + isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile, + }); + }; + } + ) +); const EuiSideNavContainer = styled.div` position: fixed; diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/container.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/container.ts index 181f35435b9a7c0..c9b85021c108e77 100644 --- a/x-pack/plugins/infra/public/pages/metrics/layouts/container.ts +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/container.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { InfraMetric } from '../../../../common/graphql/types'; import { InfraFormatterType } from '../../../lib/lib'; import { nginxLayoutCreator } from './nginx'; @@ -16,35 +17,62 @@ import { export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ { id: 'containerOverview', - label: 'Container', + label: i18n.translate('xpack.infra.metricDetailPage.containerMetricsLayout.layoutLabel', { + defaultMessage: 'Container', + }), sections: [ { id: InfraMetric.containerOverview, - label: 'Overview', + label: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.overviewSection.sectionLabel', + { + defaultMessage: 'Overview', + } + ), requires: ['docker.cpu', 'docker.memory', 'docker.network'], type: InfraMetricLayoutSectionType.gauges, visConfig: { seriesOverrides: { cpu: { - name: 'CPU Usage', + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.overviewSection.cpuUsageSeriesLabel', + { + defaultMessage: 'CPU Usage', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.percent, gaugeMax: 1, }, memory: { - name: 'Memory Usage', + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.overviewSection.memoryUsageSeriesLabel', + { + defaultMessage: 'Memory Usage', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.percent, gaugeMax: 1, }, rx: { - name: 'Inbound (RX)', + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.overviewSection.inboundRXSeriesLabel', + { + defaultMessage: 'Inbound (RX)', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.bits, formatterTemplate: '{{value}}/s', }, tx: { - name: 'Outbound (TX)', + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.overviewSection.outboundTXSeriesLabel', + { + defaultMessage: 'Outbound (TX)', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.bits, formatterTemplate: '{{value}}/s', @@ -54,7 +82,12 @@ export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.containerCpuUsage, - label: 'CPU Usage', + label: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.cpuUsageSection.sectionLabel', + { + defaultMessage: 'CPU Usage', + } + ), requires: ['docker.cpu'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -68,7 +101,12 @@ export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.containerMemory, - label: 'Memory Usage', + label: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.memoryUsageSection.sectionLabel', + { + defaultMessage: 'Memory Usage', + } + ), requires: ['docker.memory'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -82,7 +120,12 @@ export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.containerNetworkTraffic, - label: 'Network Traffic', + label: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.networkTrafficSection.sectionLabel', + { + defaultMessage: 'Network Traffic', + } + ), requires: ['docker.network'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -90,14 +133,35 @@ export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ formatterTemplate: '{{value}}/s', type: InfraMetricLayoutVisualizationType.area, seriesOverrides: { - rx: { color: theme.eui.euiColorVis1, name: 'in' }, - tx: { color: theme.eui.euiColorVis2, name: 'out' }, + rx: { + color: theme.eui.euiColorVis1, + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.networkTrafficSection.networkRxRateSeriesLabel', + { + defaultMessage: 'in', + } + ), + }, + tx: { + color: theme.eui.euiColorVis2, + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.networkTrafficSection.networkTxRateSeriesLabel', + { + defaultMessage: 'out', + } + ), + }, }, }, }, { id: InfraMetric.containerDiskIOOps, - label: 'Disk IO (Ops)', + label: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.diskIoOpsSection.sectionLabel', + { + defaultMessage: 'Disk IO (Ops)', + } + ), requires: ['docker.diskio'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -105,14 +169,35 @@ export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ formatterTemplate: '{{value}}/s', type: InfraMetricLayoutVisualizationType.area, seriesOverrides: { - read: { color: theme.eui.euiColorVis1, name: 'reads' }, - write: { color: theme.eui.euiColorVis2, name: 'writes' }, + read: { + color: theme.eui.euiColorVis1, + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.diskIoOpsSection.readRateSeriesLabel', + { + defaultMessage: 'reads', + } + ), + }, + write: { + color: theme.eui.euiColorVis2, + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.diskIoOpsSection.writeRateSeriesLabel', + { + defaultMessage: 'writes', + } + ), + }, }, }, }, { id: InfraMetric.containerDiskIOBytes, - label: 'Disk IO (Bytes)', + label: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.diskIoBytesSection.sectionLabel', + { + defaultMessage: 'Disk IO (Bytes)', + } + ), requires: ['docker.diskio'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -120,8 +205,24 @@ export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ formatterTemplate: '{{value}}/s', type: InfraMetricLayoutVisualizationType.area, seriesOverrides: { - read: { color: theme.eui.euiColorVis1, name: 'reads' }, - write: { color: theme.eui.euiColorVis2, name: 'writes' }, + read: { + color: theme.eui.euiColorVis1, + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.diskIoBytesSection.readRateSeriesLabel', + { + defaultMessage: 'reads', + } + ), + }, + write: { + color: theme.eui.euiColorVis2, + name: i18n.translate( + 'xpack.infra.metricDetailPage.containerMetricsLayout.diskIoBytesSection.writeRateSeriesLabel', + { + defaultMessage: 'writes', + } + ), + }, }, }, }, diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/host.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/host.ts index 83c4f6ad52b7d60..663d5f89802d856 100644 --- a/x-pack/plugins/infra/public/pages/metrics/layouts/host.ts +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/host.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { InfraMetric } from '../../../../common/graphql/types'; import { InfraFormatterType } from '../../../lib/lib'; import { nginxLayoutCreator } from './nginx'; @@ -16,37 +17,72 @@ import { export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ { id: 'hostOverview', - label: 'Host', + label: i18n.translate('xpack.infra.metricDetailPage.hostMetricsLayout.layoutLabel', { + defaultMessage: 'Host', + }), sections: [ { id: InfraMetric.hostSystemOverview, linkToId: 'hostOverview', - label: 'Overview', + label: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.overviewSection.sectionLabel', + { + defaultMessage: 'Overview', + } + ), requires: ['system.cpu', 'system.load', 'system.memory', 'system.network'], type: InfraMetricLayoutSectionType.gauges, visConfig: { seriesOverrides: { cpu: { - name: 'CPU Usage', + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.overviewSection.cpuUsageSeriesLabel', + { + defaultMessage: 'CPU Usage', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.percent, gaugeMax: 1, }, - load: { name: 'Load (5m)', color: theme.eui.euiColorFullShade }, + load: { + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.overviewSection.loadSeriesLabel', + { + defaultMessage: 'Load (5m)', + } + ), + color: theme.eui.euiColorFullShade, + }, memory: { - name: 'Memory Usage', + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.overviewSection.memoryCapacitySeriesLabel', + { + defaultMessage: 'Memory Usage', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.percent, gaugeMax: 1, }, rx: { - name: 'Inbound (RX)', + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.overviewSection.inboundRXSeriesLabel', + { + defaultMessage: 'Inbound (RX)', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.bits, formatterTemplate: '{{value}}/s', }, tx: { - name: 'Outbound (TX)', + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.overviewSection.outboundTXSeriesLabel', + { + defaultMessage: 'Outbound (TX)', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.bits, formatterTemplate: '{{value}}/s', @@ -56,7 +92,12 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.hostCpuUsage, - label: 'CPU Usage', + label: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.cpuUsageSection.sectionLabel', + { + defaultMessage: 'CPU Usage', + } + ), requires: ['system.cpu'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -77,20 +118,54 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.hostLoad, - label: 'Load', + label: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.loadSection.sectionLabel', + { + defaultMessage: 'Load', + } + ), requires: ['system.load'], type: InfraMetricLayoutSectionType.chart, visConfig: { seriesOverrides: { - load_1m: { color: theme.eui.euiColorVis0, name: '1m' }, - load_5m: { color: theme.eui.euiColorVis1, name: '5m' }, - load_15m: { color: theme.eui.euiColorVis3, name: '15m' }, + load_1m: { + color: theme.eui.euiColorVis0, + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.loadSection.oneMinuteSeriesLabel', + { + defaultMessage: '1m', + } + ), + }, + load_5m: { + color: theme.eui.euiColorVis1, + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.loadSection.fiveMinuteSeriesLabel', + { + defaultMessage: '5m', + } + ), + }, + load_15m: { + color: theme.eui.euiColorVis3, + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.loadSection.fifteenMinuteSeriesLabel', + { + defaultMessage: '15m', + } + ), + }, }, }, }, { id: InfraMetric.hostMemoryUsage, - label: 'MemoryUsage', + label: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.memoryUsageSection.sectionLabel', + { + defaultMessage: 'Memory Usage', + } + ), requires: ['system.memory'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -106,7 +181,12 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.hostNetworkTraffic, - label: 'Network Traffic', + label: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.networkTrafficSection.sectionLabel', + { + defaultMessage: 'Network Traffic', + } + ), requires: ['system.network'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -114,8 +194,24 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ formatterTemplate: '{{value}}/s', type: InfraMetricLayoutVisualizationType.area, seriesOverrides: { - rx: { color: theme.eui.euiColorVis1, name: 'in' }, - tx: { color: theme.eui.euiColorVis2, name: 'out' }, + rx: { + color: theme.eui.euiColorVis1, + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.networkTrafficSection.networkRxRateSeriesLabel', + { + defaultMessage: 'in', + } + ), + }, + tx: { + color: theme.eui.euiColorVis2, + name: i18n.translate( + 'xpack.infra.metricDetailPage.hostMetricsLayout.networkTrafficSection.networkTxRateSeriesLabel', + { + defaultMessage: 'out', + } + ), + }, }, }, }, @@ -128,32 +224,65 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ { id: InfraMetric.hostK8sOverview, linkToId: 'k8sOverview', - label: 'Overview', + label: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.overviewSection.sectionLabel', + { + defaultMessage: 'Overview', + } + ), requires: ['kubernetes.node'], type: InfraMetricLayoutSectionType.gauges, visConfig: { seriesOverrides: { cpucap: { - name: 'CPU Capacity', + name: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.overviewSection.cpuUsageSeriesLabel', + { + defaultMessage: 'CPU Capacity', + } + ), color: 'secondary', formatter: InfraFormatterType.percent, gaugeMax: 1, }, - load: { name: 'Load (5m)', color: 'secondary' }, + load: { + name: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.overviewSection.loadSeriesLabel', + { + defaultMessage: 'Load (5m)', + } + ), + color: 'secondary', + }, memorycap: { - name: 'Memory Capacity', + name: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.overviewSection.memoryUsageSeriesLabel', + { + defaultMessage: 'Memory Capacity', + } + ), color: 'secondary', formatter: InfraFormatterType.percent, gaugeMax: 1, }, podcap: { - name: 'Pod Capacity', + name: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.overviewSection.podCapacitySeriesLabel', + { + defaultMessage: 'Pod Capacity', + } + ), color: 'secondary', formatter: InfraFormatterType.percent, gaugeMax: 1, }, diskcap: { - name: 'Disk Capacity', + name: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.overviewSection.diskCapacitySeriesLabel', + { + defaultMessage: 'Disk Capacity', + } + ), color: 'secondary', formatter: InfraFormatterType.percent, gaugeMax: 1, @@ -163,7 +292,12 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.hostK8sCpuCap, - label: 'Node CPU Capacity', + label: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.nodeCpuCapacitySection.sectionLabel', + { + defaultMessage: 'Node CPU Capacity', + } + ), requires: ['kubernetes.node'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -176,7 +310,12 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.hostK8sMemoryCap, - label: 'Node Memory Capacity', + label: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.nodeMemoryCapacitySection.sectionLabel', + { + defaultMessage: 'Node Memory Capacity', + } + ), requires: ['kubernetes.node'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -189,7 +328,12 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.hostK8sDiskCap, - label: 'Node Disk Capacity', + label: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.nodeDiskCapacitySection.sectionLabel', + { + defaultMessage: 'Node Disk Capacity', + } + ), requires: ['kubernetes.node'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -202,7 +346,12 @@ export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.hostK8sPodCap, - label: 'Node Pod Capacity', + label: i18n.translate( + 'xpack.infra.metricDetailPage.kubernetesMetricsLayout.nodePodCapacitySection.sectionLabel', + { + defaultMessage: 'Node Pod Capacity', + } + ), requires: ['kubernetes.node'], type: InfraMetricLayoutSectionType.chart, visConfig: { diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts index f03580feb72c0f1..cee9f2ea43ab77b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { InfraMetric } from '../../../../common/graphql/types'; import { InfraFormatterType } from '../../../lib/lib'; import { @@ -20,7 +21,12 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [ sections: [ { id: InfraMetric.nginxHits, - label: 'Hits', + label: i18n.translate( + 'xpack.infra.metricDetailPage.nginxMetricsLayout.hitsSection.sectionLabel', + { + defaultMessage: 'Hits', + } + ), requires: ['nginx.access'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -36,7 +42,12 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.nginxRequestRate, - label: 'Request Rate', + label: i18n.translate( + 'xpack.infra.metricDetailPage.nginxMetricsLayout.requestRateSection.sectionLabel', + { + defaultMessage: 'Request Rate', + } + ), requires: ['nginx.statusstub'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -49,7 +60,12 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.nginxActiveConnections, - label: 'Active Connections', + label: i18n.translate( + 'xpack.infra.metricDetailPage.nginxMetricsLayout.activeConnectionsSection.sectionLabel', + { + defaultMessage: 'Active Connections', + } + ), requires: ['nginx.statusstub'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -64,7 +80,12 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.nginxRequestsPerConnection, - label: 'Requests per Connections', + label: i18n.translate( + 'xpack.infra.metricDetailPage.nginxMetricsLayout.requestsPerConnectionsSection.sectionLabel', + { + defaultMessage: 'Requests per Connections', + } + ), requires: ['nginx.statusstub'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -73,7 +94,12 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [ reqPerConns: { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.bar, - name: 'reqs per conn', + name: i18n.translate( + 'xpack.infra.metricDetailPage.nginxMetricsLayout.requestsPerConnectionsSection.reqsPerConnSeriesLabel', + { + defaultMessage: 'reqs per conn', + } + ), }, }, }, diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts index 8794acee43848bd..612ccc8c79a6bb1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { InfraMetric } from '../../../../common/graphql/types'; import { InfraFormatterType } from '../../../lib/lib'; import { nginxLayoutCreator } from './nginx'; @@ -16,35 +17,62 @@ import { export const podLayoutCreator: InfraMetricLayoutCreator = theme => [ { id: 'podOverview', - label: 'Pod Overview', + label: i18n.translate('xpack.infra.metricDetailPage.podMetricsLayout.layoutLabel', { + defaultMessage: 'Pod', + }), sections: [ { id: InfraMetric.podOverview, - label: 'Pod Overview', + label: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.overviewSection.sectionLabel', + { + defaultMessage: 'Overview', + } + ), requires: ['kubernetes.pod'], type: InfraMetricLayoutSectionType.gauges, visConfig: { seriesOverrides: { cpu: { - name: 'CPU Usage', + name: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.overviewSection.cpuUsageSeriesLabel', + { + defaultMessage: 'CPU Usage', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.percent, gaugeMax: 1, }, memory: { - name: 'Memory Usage', + name: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.overviewSection.memoryUsageSeriesLabel', + { + defaultMessage: 'Memory Usage', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.percent, gaugeMax: 1, }, rx: { - name: 'Inbound (RX)', + name: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.overviewSection.inboundRXSeriesLabel', + { + defaultMessage: 'Inbound (RX)', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.bits, formatterTemplate: '{{value}}/s', }, tx: { - name: 'Outbound (TX)', + name: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.overviewSection.outboundTXSeriesLabel', + { + defaultMessage: 'Outbound (TX)', + } + ), color: theme.eui.euiColorFullShade, formatter: InfraFormatterType.bits, formatterTemplate: '{{value}}/s', @@ -54,7 +82,12 @@ export const podLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.podCpuUsage, - label: 'CPU Usage', + label: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.cpuUsageSection.sectionLabel', + { + defaultMessage: 'CPU Usage', + } + ), requires: ['kubernetes.pod'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -66,7 +99,12 @@ export const podLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.podMemoryUsage, - label: 'Memory Usage', + label: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.memoryUsageSection.sectionLabel', + { + defaultMessage: 'Memory Usage', + } + ), requires: ['kubernetes.pod'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -81,7 +119,12 @@ export const podLayoutCreator: InfraMetricLayoutCreator = theme => [ }, { id: InfraMetric.podNetworkTraffic, - label: 'Network Traffic', + label: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.networkTrafficSection.sectionLabel', + { + defaultMessage: 'Network Traffic', + } + ), requires: ['kubernetes.pod'], type: InfraMetricLayoutSectionType.chart, visConfig: { @@ -89,8 +132,24 @@ export const podLayoutCreator: InfraMetricLayoutCreator = theme => [ formatterTemplate: '{{value}}/s', type: InfraMetricLayoutVisualizationType.area, seriesOverrides: { - rx: { color: theme.eui.euiColorVis1, name: 'in' }, - tx: { color: theme.eui.euiColorVis2, name: 'out' }, + rx: { + color: theme.eui.euiColorVis1, + name: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.networkTrafficSection.networkRxRateSeriesLabel', + { + defaultMessage: 'in', + } + ), + }, + tx: { + color: theme.eui.euiColorVis2, + name: i18n.translate( + 'xpack.infra.metricDetailPage.podMetricsLayout.networkTrafficSection.networkTxRateSeriesLabel', + { + defaultMessage: 'out', + } + ), + }, }, }, }, diff --git a/x-pack/plugins/infra/server/graphql/index.ts b/x-pack/plugins/infra/server/graphql/index.ts index 7fb3a92330352d8..20aa0b75097d962 100644 --- a/x-pack/plugins/infra/server/graphql/index.ts +++ b/x-pack/plugins/infra/server/graphql/index.ts @@ -6,8 +6,8 @@ import { rootSchema } from '../../common/graphql/root/schema.gql'; import { sharedSchema } from '../../common/graphql/shared/schema.gql'; -import { capabilitiesSchema } from './capabilities/schema.gql'; import { logEntriesSchema } from './log_entries/schema.gql'; +import { metadataSchema } from './metadata/schema.gql'; import { metricsSchema } from './metrics/schema.gql'; import { nodesSchema } from './nodes/schema.gql'; import { sourceStatusSchema } from './source_status/schema.gql'; @@ -16,7 +16,7 @@ import { sourcesSchema } from './sources/schema.gql'; export const schemas = [ rootSchema, sharedSchema, - capabilitiesSchema, + metadataSchema, logEntriesSchema, nodesSchema, sourcesSchema, diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.js b/x-pack/plugins/infra/server/graphql/metadata/index.ts similarity index 70% rename from x-pack/plugins/canvas/public/lib/create_handlers.js rename to x-pack/plugins/infra/server/graphql/metadata/index.ts index 93247210eb2911f..cda731bdaa9b623 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.js +++ b/x-pack/plugins/infra/server/graphql/metadata/index.ts @@ -4,8 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export function createHandlers(/*socket*/) { - return { - environment: 'client', - }; -} +export { createMetadataResolvers } from './resolvers'; +export { metadataSchema } from './schema.gql'; diff --git a/x-pack/plugins/infra/server/graphql/capabilities/resolvers.ts b/x-pack/plugins/infra/server/graphql/metadata/resolvers.ts similarity index 54% rename from x-pack/plugins/infra/server/graphql/capabilities/resolvers.ts rename to x-pack/plugins/infra/server/graphql/metadata/resolvers.ts index fa242f67b230fbf..0d4c66643721c1c 100644 --- a/x-pack/plugins/infra/server/graphql/capabilities/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/metadata/resolvers.ts @@ -6,31 +6,26 @@ import { InfraSourceResolvers } from '../../../common/graphql/types'; import { InfraResolvedResult, InfraResolverOf } from '../../lib/adapters/framework'; -import { InfraCapabilitiesDomain } from '../../lib/domains/capabilities_domain'; +import { InfraMetadataDomain } from '../../lib/domains/metadata_domain'; import { InfraContext } from '../../lib/infra_types'; import { QuerySourceResolver } from '../sources/resolvers'; -type InfraSourceCapabilitiesByNodeResolver = InfraResolverOf< - InfraSourceResolvers.CapabilitiesByNodeResolver, +type InfraSourceMetadataByNodeResolver = InfraResolverOf< + InfraSourceResolvers.MetadataByNodeResolver, InfraResolvedResult, InfraContext >; -export const createCapabilitiesResolvers = (libs: { - capabilities: InfraCapabilitiesDomain; +export const createMetadataResolvers = (libs: { + metadata: InfraMetadataDomain; }): { InfraSource: { - capabilitiesByNode: InfraSourceCapabilitiesByNodeResolver; + metadataByNode: InfraSourceMetadataByNodeResolver; }; } => ({ InfraSource: { - async capabilitiesByNode(source, args, { req }) { - const result = await libs.capabilities.getCapabilities( - req, - source.id, - args.nodeName, - args.nodeType - ); + async metadataByNode(source, args, { req }) { + const result = await libs.metadata.getMetadata(req, source.id, args.nodeName, args.nodeType); return result; }, }, diff --git a/x-pack/plugins/infra/server/graphql/capabilities/schema.gql.ts b/x-pack/plugins/infra/server/graphql/metadata/schema.gql.ts similarity index 53% rename from x-pack/plugins/infra/server/graphql/capabilities/schema.gql.ts rename to x-pack/plugins/infra/server/graphql/metadata/schema.gql.ts index 9a97ff29eeb857d..1a0e40d8f0b8247 100644 --- a/x-pack/plugins/infra/server/graphql/capabilities/schema.gql.ts +++ b/x-pack/plugins/infra/server/graphql/metadata/schema.gql.ts @@ -6,15 +6,15 @@ import gql from 'graphql-tag'; -export const capabilitiesSchema = gql` - "One specific capability available on a node. A capability corresponds to a fileset or metricset" - type InfraNodeCapability { +export const metadataSchema = gql` + "One metadata entry for a node." + type InfraNodeMetadata { name: String! source: String! } extend type InfraSource { - "A hierarchy of capabilities available on nodes" - capabilitiesByNode(nodeName: String!, nodeType: InfraNodeType!): [InfraNodeCapability]! + "A hierarchy of metadata entries by node" + metadataByNode(nodeName: String!, nodeType: InfraNodeType!): [InfraNodeMetadata]! } `; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index eb68e44af6423ad..a89d3cb6e817006 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -6,8 +6,8 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools'; import { schemas } from './graphql'; -import { createCapabilitiesResolvers } from './graphql/capabilities'; import { createLogEntriesResolvers } from './graphql/log_entries'; +import { createMetadataResolvers } from './graphql/metadata'; import { createMetricResolvers } from './graphql/metrics/resolvers'; import { createNodeResolvers } from './graphql/nodes'; import { createSourceStatusResolvers } from './graphql/source_status'; @@ -18,7 +18,7 @@ import { initLegacyLoggingRoutes } from './logging_legacy'; export const initInfraServer = (libs: InfraBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ - createCapabilitiesResolvers(libs) as IResolvers, + createMetadataResolvers(libs) as IResolvers, createLogEntriesResolvers(libs) as IResolvers, createNodeResolvers(libs) as IResolvers, createSourcesResolvers(libs) as IResolvers, diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 28378248a6384eb..23de4c7b2b81d03 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -148,15 +148,15 @@ export interface InfraDateRangeAggregationResponse { buckets: InfraDateRangeAggregationBucket[]; } -export interface InfraCapabilityAggregationBucket { +export interface InfraMetadataAggregationBucket { key: string; names?: { - buckets: InfraCapabilityAggregationBucket[]; + buckets: InfraMetadataAggregationBucket[]; }; } -export interface InfraCapabilityAggregationResponse { - buckets: InfraCapabilityAggregationBucket[]; +export interface InfraMetadataAggregationResponse { + buckets: InfraMetadataAggregationBucket[]; } export interface InfraFieldsResponse { diff --git a/x-pack/plugins/infra/server/lib/adapters/capabilities/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/metadata/adapter_types.ts similarity index 66% rename from x-pack/plugins/infra/server/lib/adapters/capabilities/adapter_types.ts rename to x-pack/plugins/infra/server/lib/adapters/metadata/adapter_types.ts index 1486d0d9e2e386f..e7aece48c7cd017 100644 --- a/x-pack/plugins/infra/server/lib/adapters/capabilities/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metadata/adapter_types.ts @@ -5,19 +5,19 @@ */ import { InfraSourceConfiguration } from '../../sources'; -import { InfraCapabilityAggregationBucket, InfraFrameworkRequest } from '../framework'; +import { InfraFrameworkRequest, InfraMetadataAggregationBucket } from '../framework'; -export interface InfraCapabilitiesAdapter { - getMetricCapabilities( +export interface InfraMetadataAdapter { + getMetricMetadata( req: InfraFrameworkRequest, sourceConfiguration: InfraSourceConfiguration, nodeName: string, nodeType: string - ): Promise; - getLogCapabilities( + ): Promise; + getLogMetadata( req: InfraFrameworkRequest, sourceConfiguration: InfraSourceConfiguration, nodeName: string, nodeType: string - ): Promise; + ): Promise; } diff --git a/x-pack/plugins/infra/server/lib/adapters/capabilities/elasticsearch_capabilities_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts similarity index 85% rename from x-pack/plugins/infra/server/lib/adapters/capabilities/elasticsearch_capabilities_adapter.ts rename to x-pack/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts index 049fb864dcbc098..7972154124ee6b2 100644 --- a/x-pack/plugins/infra/server/lib/adapters/capabilities/elasticsearch_capabilities_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts @@ -7,24 +7,24 @@ import { InfraSourceConfiguration } from '../../sources'; import { InfraBackendFrameworkAdapter, - InfraCapabilityAggregationBucket, - InfraCapabilityAggregationResponse, InfraFrameworkRequest, + InfraMetadataAggregationBucket, + InfraMetadataAggregationResponse, } from '../framework'; -import { InfraCapabilitiesAdapter } from './adapter_types'; +import { InfraMetadataAdapter } from './adapter_types'; -export class ElasticsearchCapabilitiesAdapter implements InfraCapabilitiesAdapter { +export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter { private framework: InfraBackendFrameworkAdapter; constructor(framework: InfraBackendFrameworkAdapter) { this.framework = framework; } - public async getMetricCapabilities( + public async getMetricMetadata( req: InfraFrameworkRequest, sourceConfiguration: InfraSourceConfiguration, nodeName: string, nodeType: 'host' | 'container' | 'pod' - ): Promise { + ): Promise { const idFieldName = getIdFieldName(sourceConfiguration, nodeType); const metricQuery = { index: sourceConfiguration.metricAlias, @@ -58,7 +58,7 @@ export class ElasticsearchCapabilitiesAdapter implements InfraCapabilitiesAdapte const response = await this.framework.callWithRequest< any, - { metrics?: InfraCapabilityAggregationResponse } + { metrics?: InfraMetadataAggregationResponse } >(req, 'search', metricQuery); return response.aggregations && response.aggregations.metrics @@ -66,12 +66,12 @@ export class ElasticsearchCapabilitiesAdapter implements InfraCapabilitiesAdapte : []; } - public async getLogCapabilities( + public async getLogMetadata( req: InfraFrameworkRequest, sourceConfiguration: InfraSourceConfiguration, nodeName: string, nodeType: 'host' | 'container' | 'pod' - ): Promise { + ): Promise { const idFieldName = getIdFieldName(sourceConfiguration, nodeType); const logQuery = { index: sourceConfiguration.logAlias, @@ -105,7 +105,7 @@ export class ElasticsearchCapabilitiesAdapter implements InfraCapabilitiesAdapte const response = await this.framework.callWithRequest< any, - { metrics?: InfraCapabilityAggregationResponse } + { metrics?: InfraMetadataAggregationResponse } >(req, 'search', logQuery); return response.aggregations && response.aggregations.metrics diff --git a/x-pack/plugins/infra/server/lib/adapters/capabilities/index.ts b/x-pack/plugins/infra/server/lib/adapters/metadata/index.ts similarity index 100% rename from x-pack/plugins/infra/server/lib/adapters/capabilities/index.ts rename to x-pack/plugins/infra/server/lib/adapters/metadata/index.ts diff --git a/x-pack/plugins/infra/server/lib/compose/kibana.ts b/x-pack/plugins/infra/server/lib/compose/kibana.ts index 23c5c3a45bd2317..635e413e152e5cd 100644 --- a/x-pack/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/plugins/infra/server/lib/compose/kibana.ts @@ -6,18 +6,18 @@ import { Server } from 'hapi'; -import { ElasticsearchCapabilitiesAdapter } from '../adapters/capabilities/elasticsearch_capabilities_adapter'; import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kibana_configuration_adapter'; import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter'; import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter'; +import { ElasticsearchMetadataAdapter } from '../adapters/metadata/elasticsearch_metadata_adapter'; import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter'; import { ElasticsearchNodesAdapter } from '../adapters/nodes/elasticsearch_nodes_adapter'; import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status'; import { InfraConfigurationSourcesAdapter } from '../adapters/sources/configuration_sources_adapter'; -import { InfraCapabilitiesDomain } from '../domains/capabilities_domain'; import { InfraFieldsDomain } from '../domains/fields_domain'; import { InfraLogEntriesDomain } from '../domains/log_entries_domain'; +import { InfraMetadataDomain } from '../domains/metadata_domain'; import { InfraMetricsDomain } from '../domains/metrics_domain'; import { InfraNodesDomain } from '../domains/nodes_domain'; import { InfraBackendLibs, InfraConfiguration, InfraDomainLibs } from '../infra_types'; @@ -33,7 +33,7 @@ export function compose(server: Server): InfraBackendLibs { }); const domainLibs: InfraDomainLibs = { - capabilities: new InfraCapabilitiesDomain(new ElasticsearchCapabilitiesAdapter(framework), { + metadata: new InfraMetadataDomain(new ElasticsearchMetadataAdapter(framework), { sources, }), fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { diff --git a/x-pack/plugins/infra/server/lib/domains/capabilities_domain/index.ts b/x-pack/plugins/infra/server/lib/domains/metadata_domain/index.ts similarity index 86% rename from x-pack/plugins/infra/server/lib/domains/capabilities_domain/index.ts rename to x-pack/plugins/infra/server/lib/domains/metadata_domain/index.ts index 525e60a00f78630..8095e8424873aa8 100644 --- a/x-pack/plugins/infra/server/lib/domains/capabilities_domain/index.ts +++ b/x-pack/plugins/infra/server/lib/domains/metadata_domain/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './capabilities_domain'; +export * from './metadata_domain'; diff --git a/x-pack/plugins/infra/server/lib/domains/capabilities_domain/capabilities_domain.ts b/x-pack/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts similarity index 52% rename from x-pack/plugins/infra/server/lib/domains/capabilities_domain/capabilities_domain.ts rename to x-pack/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts index 2b3f73a9eccf7d3..9453061f590ec3c 100644 --- a/x-pack/plugins/infra/server/lib/domains/capabilities_domain/capabilities_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts @@ -4,54 +4,49 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraCapabilitiesAdapter } from '../../adapters/capabilities'; -import { InfraCapabilityAggregationBucket, InfraFrameworkRequest } from '../../adapters/framework'; +import { InfraFrameworkRequest, InfraMetadataAggregationBucket } from '../../adapters/framework'; +import { InfraMetadataAdapter } from '../../adapters/metadata'; import { InfraSources } from '../../sources'; -export class InfraCapabilitiesDomain { +export class InfraMetadataDomain { constructor( - private readonly adapter: InfraCapabilitiesAdapter, + private readonly adapter: InfraMetadataAdapter, private readonly libs: { sources: InfraSources } ) {} - public async getCapabilities( + public async getMetadata( req: InfraFrameworkRequest, sourceId: string, nodeName: string, nodeType: string ) { const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); - const metricsPromise = this.adapter.getMetricCapabilities( - req, - sourceConfiguration, - nodeName, - nodeType - ); - const logsPromise = this.adapter.getLogCapabilities( + const metricsPromise = this.adapter.getMetricMetadata( req, sourceConfiguration, nodeName, nodeType ); + const logsPromise = this.adapter.getLogMetadata(req, sourceConfiguration, nodeName, nodeType); const metrics = await metricsPromise; const logs = await logsPromise; - const metricCapabilities = pickCapabilities(metrics).map(metricCapability => { - return { name: metricCapability, source: 'metrics' }; + const metricMetadata = pickMetadata(metrics).map(entry => { + return { name: entry, source: 'metrics' }; }); - const logCapabilities = pickCapabilities(logs).map(logCapability => { - return { name: logCapability, source: 'logs' }; + const logMetadata = pickMetadata(logs).map(entry => { + return { name: entry, source: 'logs' }; }); - return metricCapabilities.concat(logCapabilities); + return metricMetadata.concat(logMetadata); } } -const pickCapabilities = (buckets: InfraCapabilityAggregationBucket[]): string[] => { +const pickMetadata = (buckets: InfraMetadataAggregationBucket[]): string[] => { if (buckets) { - const capabilities = buckets + const metadata = buckets .map(module => { if (module.names) { return module.names.buckets.map(name => { @@ -62,7 +57,7 @@ const pickCapabilities = (buckets: InfraCapabilityAggregationBucket[]): string[] } }) .reduce((a: string[], b: string[]) => a.concat(b), []); - return capabilities; + return metadata; } else { return []; } diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 2a24a65daf8d642..3bb2fe32474169b 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -6,16 +6,16 @@ import { InfraConfigurationAdapter } from './adapters/configuration'; import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework'; -import { InfraCapabilitiesDomain } from './domains/capabilities_domain'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; +import { InfraMetadataDomain } from './domains/metadata_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; import { InfraNodesDomain } from './domains/nodes_domain'; import { InfraSourceStatus } from './source_status'; import { InfraSourceConfigurations, InfraSources } from './sources'; export interface InfraDomainLibs { - capabilities: InfraCapabilitiesDomain; + metadata: InfraMetadataDomain; fields: InfraFieldsDomain; logEntries: InfraLogEntriesDomain; nodes: InfraNodesDomain; diff --git a/x-pack/plugins/ml/public/_app.scss b/x-pack/plugins/ml/public/_app.scss new file mode 100644 index 000000000000000..6dfae7a8921f35a --- /dev/null +++ b/x-pack/plugins/ml/public/_app.scss @@ -0,0 +1,30 @@ +// ML has app specific coloring for it's various warning levels. +// These are used almost everywhere. + +.ml-icon-severity-critical, +.ml-icon-severity-major, +.ml-icon-severity-minor, +.ml-icon-severity-warning, +.ml-icon-severity-unknown { + text-shadow: 1px 1px 1px $euiColorLightShade; +} + +.ml-icon-severity-critical { + color: $mlColorCriticalText; +} + +.ml-icon-severity-major { + color: $mlColorMajorText; +} + +.ml-icon-severity-minor { + color: $mlColorMinorText; +} + +.ml-icon-severity-warning { + color: $mlColorWarningText; +} + +.ml-icon-severity-unknown { + color: $mlColorUnknownText; +} diff --git a/x-pack/plugins/ml/public/_hacks.scss b/x-pack/plugins/ml/public/_hacks.scss new file mode 100644 index 000000000000000..68aa3e075ffc6f9 --- /dev/null +++ b/x-pack/plugins/ml/public/_hacks.scss @@ -0,0 +1,60 @@ +// These are hacks that were near ML's root. Unsure if they still need to be here. +.tab-jobs, +.edit-job-modal, +.create-watch-modal { + label { + display: inline-block; + } + + .validation-error { + margin-top: $euiSizeXS; + } +} + +// ML specific bootstrap hacks +.button-wrapper { + display: inline; +} + +.button-wrapper.disabled .kuiButton[disabled] { + pointer-events: none; +} + +.button-wrapper.disabled { + cursor: not-allowed; +} + +// ML bootstrap-select hacks that sit on top of Kibana hacks that often fight with KUI +// Should go away when EUI is fully adopted +.ui-select-match { + .btn-default[disabled], + .btn-default[disabled]:hover, + .btn-default[disabled]:focus { + background-color: $euiColorLightShade; + border-color: $euiColorLightShade; + opacity: 1; + + .ui-select-placeholder { + color: $euiColorDarkShade; + } + } + } + +.ui-select-container input[type="search"]::placeholder { + color: $euiColorDarkShade; +} + +.ui-select-container input[type="search"]:focus { + box-shadow: none; +} + +.ui-select-multiple.ui-select-bootstrap input.ui-select-search { + font-size: $euiFontSizeS; + padding: 5px 10px; // Matches current padding hacks from other parts of Kibana +} + + +// SASSTODO: Remove all the floats +.clear, .clearfix { + clear: both; +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/_variables.scss b/x-pack/plugins/ml/public/_variables.scss new file mode 100644 index 000000000000000..159a1ffd45dd507 --- /dev/null +++ b/x-pack/plugins/ml/public/_variables.scss @@ -0,0 +1,11 @@ +$mlColorCritical: #fe5050; +$mlColorMajor: #fba740; +$mlColorMinor: #fdec25; +$mlColorWarning: #8bc8fb; +$mlColorUnknown: #c0c0c0; + +$mlColorCriticalText: makeHighContrastColor($mlColorCritical, $euiColorEmptyShade); +$mlColorMajorText: makeHighContrastColor($mlColorMajor, $euiColorEmptyShade); +$mlColorMinorText: makeHighContrastColor($mlColorMinor, $euiColorEmptyShade); +$mlColorWarningText: makeHighContrastColor($mlColorWarning, $euiColorEmptyShade); +$mlColorUnknownText: $euiColorDarkShade; diff --git a/x-pack/plugins/ml/public/app.js b/x-pack/plugins/ml/public/app.js index 1af1b28286b985e..5b645e0425f853c 100644 --- a/x-pack/plugins/ml/public/app.js +++ b/x-pack/plugins/ml/public/app.js @@ -14,7 +14,6 @@ import 'ui-bootstrap'; import 'ui/persisted_log'; import 'ui/autoload/all'; -import 'plugins/ml/styles/main.less'; import 'plugins/ml/access_denied'; import 'plugins/ml/factories/listener_factory'; import 'plugins/ml/factories/state_factory'; diff --git a/x-pack/plugins/ml/public/components/anomalies_table/styles/main.less b/x-pack/plugins/ml/public/components/anomalies_table/_anomalies_table.scss similarity index 75% rename from x-pack/plugins/ml/public/components/anomalies_table/styles/main.less rename to x-pack/plugins/ml/public/components/anomalies_table/_anomalies_table.scss index 7469f1b529100fe..cfcf57a96a9bc5d 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/styles/main.less +++ b/x-pack/plugins/ml/public/components/anomalies_table/_anomalies_table.scss @@ -1,3 +1,4 @@ + // SASSTODO: This file has several direct EUI overwrites that need to be removed .ml-anomalies-table { .ml-icon-severity-critical, .ml-icon-severity-major, @@ -8,42 +9,43 @@ text-shadow: none; } - + // SASSTODO: Should only be three options, logic moved to the JS, where EuiIcon accepts a color .ml-icon-severity-critical { .euiIcon { - fill: #fe5050; + fill: $mlColorCriticalText; } } .ml-icon-severity-major { .euiIcon { - fill: #fba740; + fill: $mlColorMajorText; } } .ml-icon-severity-minor { .euiIcon { - fill: #fdec25; + fill: $mlColorMinorText; } } .ml-icon-severity-warning { .euiIcon { - fill: #8bc8fb; + fill: $mlColorWarningText; } } .ml-icon-severity-unknown { .euiIcon { - fill: #c0c0c0; + fill: $mlColorUnknownText; } } tr th:first-child, tr td:first-child { - width: 32px; + width: $euiSizeXL; } + // SASSTODO: These need to be separate classes, not overrides .euiTableCellContent { .euiHealth { font-size: inherit; @@ -65,13 +67,13 @@ } .detector-rules-icon { - margin-left: 3px; + margin-left: $euiSizeXS; opacity: 0.5; } } .euiContextMenuItem { - min-width: 150px + min-width: 150px; } .category-example { @@ -84,7 +86,7 @@ } .ml-anomalies-table-details { - padding: 4px 32px; + padding: $euiSizeXS $euiSizeXL; max-height: 1000px; overflow-y: auto; diff --git a/x-pack/plugins/ml/public/components/anomalies_table/_index.scss b/x-pack/plugins/ml/public/components/anomalies_table/_index.scss new file mode 100644 index 000000000000000..20e40e840a4bf8b --- /dev/null +++ b/x-pack/plugins/ml/public/components/anomalies_table/_index.scss @@ -0,0 +1 @@ +@import 'anomalies_table'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_service.js b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_service.js index 2c7147650c221d9..28183e92db54693 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_service.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table_service.js @@ -11,7 +11,7 @@ * anomalies table component. */ -import { listenerFactoryProvider } from 'plugins/ml/factories/listener_factory'; +import { listenerFactoryProvider } from '../../factories/listener_factory'; class AnomaliesTableService { constructor() { diff --git a/x-pack/plugins/ml/public/components/anomalies_table/index.js b/x-pack/plugins/ml/public/components/anomalies_table/index.js index f9fb2bc0dafa092..e2750dff9ee62b5 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/index.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/index.js @@ -6,5 +6,4 @@ import './anomalies_table_directive'; -import './anomalies_table_service.js'; -import './styles/main.less'; +import './anomalies_table_service.js'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/chart_tooltip/styles/main.less b/x-pack/plugins/ml/public/components/chart_tooltip/_chart_tooltip.scss similarity index 51% rename from x-pack/plugins/ml/public/components/chart_tooltip/styles/main.less rename to x-pack/plugins/ml/public/components/chart_tooltip/_chart_tooltip.scss index be51f28a38f8961..f30220eed25134a 100644 --- a/x-pack/plugins/ml/public/components/chart_tooltip/styles/main.less +++ b/x-pack/plugins/ml/public/components/chart_tooltip/_chart_tooltip.scss @@ -1,12 +1,12 @@ .ml-chart-tooltip { position: absolute; - border: 2px solid #303030; + border: 2px solid $euiColorDarkestShade; border-radius: 5px; padding: 7px 5px; - color: #ffffff; - background-color: #303030; - font-family: Roboto, Droid, Helvetica Neue, Helvetica, Arial, sans-serif; - font-size: 12px; + color: $euiColorEmptyShade; + background-color: $euiColorDarkestShade; + font-family: $euiFontFamily; + font-size: $euiFontSizeXS; opacity: 0; display: none; white-space: nowrap; @@ -18,14 +18,10 @@ text-overflow: ellipsis; hr { - margin-top: 3px; - margin-bottom: 3px; + margin-top: $euiSizeXS; + margin-bottom: $euiSizeXS; border: none; height: 1px; - background-color: #95a5a6; - } - - .centered-text { - text-align: center; + background-color: $euiColorMediumShade; } } diff --git a/x-pack/plugins/ml/public/components/chart_tooltip/_index.scss b/x-pack/plugins/ml/public/components/chart_tooltip/_index.scss new file mode 100644 index 000000000000000..11b36a0a21001ee --- /dev/null +++ b/x-pack/plugins/ml/public/components/chart_tooltip/_index.scss @@ -0,0 +1 @@ +@import 'chart_tooltip'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/chart_tooltip/index.js b/x-pack/plugins/ml/public/components/chart_tooltip/index.js index 7bc2781a62bb4d4..dce4cc44e2526a5 100644 --- a/x-pack/plugins/ml/public/components/chart_tooltip/index.js +++ b/x-pack/plugins/ml/public/components/chart_tooltip/index.js @@ -6,5 +6,4 @@ -import './chart_tooltip'; -import './styles/main.less'; +import './chart_tooltip'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/confirm_modal/_confirm_modal.scss b/x-pack/plugins/ml/public/components/confirm_modal/_confirm_modal.scss new file mode 100644 index 000000000000000..55ab91b0381f52d --- /dev/null +++ b/x-pack/plugins/ml/public/components/confirm_modal/_confirm_modal.scss @@ -0,0 +1,25 @@ +.confirm-modal { + padding: $euiSizeL; + cursor: auto; + + // SASSTODO: Needs a proper selector + h3 { + margin-top: 0px; + } + + .modal-title { + font-weight: $euiFontWeightBold; + padding-bottom: $euiSizeL; + } + + .modal-body { + padding: 0px; + padding-bottom: $euiSizeL; + } + + .modal-footer { + padding: 0px; + padding-top: $euiSizeL; + } + +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/confirm_modal/_index.scss b/x-pack/plugins/ml/public/components/confirm_modal/_index.scss new file mode 100644 index 000000000000000..61d12ba2a52d3b6 --- /dev/null +++ b/x-pack/plugins/ml/public/components/confirm_modal/_index.scss @@ -0,0 +1 @@ +@import 'confirm_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/confirm_modal/index.js b/x-pack/plugins/ml/public/components/confirm_modal/index.js index ff61dbe181f8ea8..8a28055bcd47821 100644 --- a/x-pack/plugins/ml/public/components/confirm_modal/index.js +++ b/x-pack/plugins/ml/public/components/confirm_modal/index.js @@ -8,4 +8,3 @@ import './confirm_modal_service'; import './confirm_modal_controller'; -import './styles/main.less'; diff --git a/x-pack/plugins/ml/public/components/confirm_modal/styles/main.less b/x-pack/plugins/ml/public/components/confirm_modal/styles/main.less deleted file mode 100644 index bd5d2f55af21f57..000000000000000 --- a/x-pack/plugins/ml/public/components/confirm_modal/styles/main.less +++ /dev/null @@ -1,24 +0,0 @@ -.confirm-modal { - padding:20px; - cursor: auto; - - h3 { - margin-top: 0px; - } - - .modal-title { - font-weight: bold; - padding-bottom: 20px; - } - - .modal-body { - padding: 0px; - padding-bottom: 20px; - } - - .modal-footer { - padding: 0px; - padding-top: 20px; - } - -} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/controls/styles/main.less b/x-pack/plugins/ml/public/components/controls/_controls.scss similarity index 51% rename from x-pack/plugins/ml/public/components/controls/styles/main.less rename to x-pack/plugins/ml/public/components/controls/_controls.scss index 3a4dacb750c96eb..2b46e2d2cd35d32 100644 --- a/x-pack/plugins/ml/public/components/controls/styles/main.less +++ b/x-pack/plugins/ml/public/components/controls/_controls.scss @@ -1,16 +1,16 @@ .ml-table-controls { label { - font-size: 12px; - padding: 0px 0px 5px 5px; + font-size: $euiFontSizeXS; + padding: 0px 0px $euiSizeXS $euiSizeXS; } .ml-table-controls-element { display: inline-block; - padding-left: 15px; + padding-left: $euiSize; } select { - font-size: 13px; + font-size: $euiFontSizeXS; font-style: normal; } } diff --git a/x-pack/plugins/ml/public/components/controls/_index.scss b/x-pack/plugins/ml/public/components/controls/_index.scss new file mode 100644 index 000000000000000..7bb8b08c6585905 --- /dev/null +++ b/x-pack/plugins/ml/public/components/controls/_index.scss @@ -0,0 +1,3 @@ +@import 'controls'; +@import 'controls_select/index'; +@import 'select_severity/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/controls/controls_select/styles/main.less b/x-pack/plugins/ml/public/components/controls/controls_select/_controls_select.scss similarity index 52% rename from x-pack/plugins/ml/public/components/controls/controls_select/styles/main.less rename to x-pack/plugins/ml/public/components/controls/controls_select/_controls_select.scss index 5fb992081e0bc36..fc489d18feb05e5 100644 --- a/x-pack/plugins/ml/public/components/controls/controls_select/styles/main.less +++ b/x-pack/plugins/ml/public/components/controls/controls_select/_controls_select.scss @@ -14,32 +14,31 @@ ml-controls-select { .dropdown-menu { min-width: 120px; - font-size: 13px; - } + font-size: $euiFontSizeXS; - .dropdown-menu > li > a { - color: #444444; - text-decoration: none; - } + > li > a { + color: $euiColorDarkestShade;; + text-decoration: none; - .dropdown-menu > li > a:hover, - .dropdown-menu > li > a:active, - .dropdown-menu > li > a:focus { - color: #ffffff; - box-shadow: none; + &:hover, &:active, &:focus { + color: $euiColorEmptyShade; + box-shadow: none; + } + } } button.dropdown-toggle { text-align: left; - margin-bottom: 3px; + margin-bottom: $euiSizeXS; + // SASSTODO: Needs more specific selectors span { - font-size: 13px; + font-size: $euiSizeXS; } } button.dropdown-toggle:hover, button.dropdown-toggle:focus { - color: #444444; + color: $euiColorDarkestShade; } } diff --git a/x-pack/plugins/ml/public/components/controls/controls_select/_index.scss b/x-pack/plugins/ml/public/components/controls/controls_select/_index.scss new file mode 100644 index 000000000000000..4d58129f311b215 --- /dev/null +++ b/x-pack/plugins/ml/public/components/controls/controls_select/_index.scss @@ -0,0 +1 @@ +@import 'controls_select'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/controls/controls_select/index.js b/x-pack/plugins/ml/public/components/controls/controls_select/index.js index 99398c1b766a183..06074fc5219af4d 100644 --- a/x-pack/plugins/ml/public/components/controls/controls_select/index.js +++ b/x-pack/plugins/ml/public/components/controls/controls_select/index.js @@ -6,5 +6,4 @@ -import './styles/main.less'; import './controls_select_directive.js'; diff --git a/x-pack/plugins/ml/public/components/controls/index.js b/x-pack/plugins/ml/public/components/controls/index.js index 1ae5706692585cb..1b9348db9c51630 100644 --- a/x-pack/plugins/ml/public/components/controls/index.js +++ b/x-pack/plugins/ml/public/components/controls/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './checkbox_showcharts'; import './controls_select'; import './select_interval'; diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/_index.scss b/x-pack/plugins/ml/public/components/controls/select_severity/_index.scss new file mode 100644 index 000000000000000..f238b65c9b95523 --- /dev/null +++ b/x-pack/plugins/ml/public/components/controls/select_severity/_index.scss @@ -0,0 +1 @@ +@import 'select_severity'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/styles/main.less b/x-pack/plugins/ml/public/components/controls/select_severity/_select_severity.scss similarity index 73% rename from x-pack/plugins/ml/public/components/controls/select_severity/styles/main.less rename to x-pack/plugins/ml/public/components/controls/select_severity/_select_severity.scss index 564fed3084e4dc9..2edfe612183d0e9 100644 --- a/x-pack/plugins/ml/public/components/controls/select_severity/styles/main.less +++ b/x-pack/plugins/ml/public/components/controls/select_severity/_select_severity.scss @@ -1,3 +1,4 @@ +// SASSTODO: Should be removed .ml-select-severity { .euiFormControlLayoutClearButton { display: none; diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js index 03c608082b98205..217b6a361a1f0ca 100644 --- a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js +++ b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js @@ -20,8 +20,6 @@ import { EuiText, } from '@elastic/eui'; -import './styles/main.less'; - import { getSeverityColor } from '../../../../common/util/anomaly_utils'; const OPTIONS = [ diff --git a/x-pack/plugins/ml/public/components/data_recognizer/styles/main.less b/x-pack/plugins/ml/public/components/data_recognizer/_data_recognizer.scss similarity index 69% rename from x-pack/plugins/ml/public/components/data_recognizer/styles/main.less rename to x-pack/plugins/ml/public/components/data_recognizer/_data_recognizer.scss index cab0706e04b1c77..a91dd699032b12a 100644 --- a/x-pack/plugins/ml/public/components/data_recognizer/styles/main.less +++ b/x-pack/plugins/ml/public/components/data_recognizer/_data_recognizer.scss @@ -1,5 +1,5 @@ ml-data-recognizer { .ml-data-recognizer-logo { - width: 32px; + width: $euiSizeXL; } -} +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/data_recognizer/_index.scss b/x-pack/plugins/ml/public/components/data_recognizer/_index.scss new file mode 100644 index 000000000000000..67cc4372ea62254 --- /dev/null +++ b/x-pack/plugins/ml/public/components/data_recognizer/_index.scss @@ -0,0 +1 @@ +@import 'data_recognizer'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/data_recognizer/index.js b/x-pack/plugins/ml/public/components/data_recognizer/index.js index ae6ec1967423430..68952807d424025 100644 --- a/x-pack/plugins/ml/public/components/data_recognizer/index.js +++ b/x-pack/plugins/ml/public/components/data_recognizer/index.js @@ -8,4 +8,3 @@ import './data_recognizer_directive'; import './data_recognizer'; -import './styles/main.less'; diff --git a/x-pack/plugins/ml/public/components/documentation_help_link/_documentation_help_link.scss b/x-pack/plugins/ml/public/components/documentation_help_link/_documentation_help_link.scss new file mode 100644 index 000000000000000..cf14dfcc0d3b1ab --- /dev/null +++ b/x-pack/plugins/ml/public/components/documentation_help_link/_documentation_help_link.scss @@ -0,0 +1,6 @@ +// SASSTODO: Make a defined selector +.documentation-help-link { + i { + margin-left: $euiSizeXS; + } +} diff --git a/x-pack/plugins/ml/public/components/documentation_help_link/_index.scss b/x-pack/plugins/ml/public/components/documentation_help_link/_index.scss new file mode 100644 index 000000000000000..762af88c62e5889 --- /dev/null +++ b/x-pack/plugins/ml/public/components/documentation_help_link/_index.scss @@ -0,0 +1 @@ +@import 'documentation_help_link'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/documentation_help_link/documentation_help_link.js b/x-pack/plugins/ml/public/components/documentation_help_link/documentation_help_link.js index 7faf675199879a5..8762a4d4c18f5c0 100644 --- a/x-pack/plugins/ml/public/components/documentation_help_link/documentation_help_link.js +++ b/x-pack/plugins/ml/public/components/documentation_help_link/documentation_help_link.js @@ -9,8 +9,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './styles/main.less'; - import { metadata } from 'ui/metadata'; import { uiModules } from 'ui/modules'; diff --git a/x-pack/plugins/ml/public/components/documentation_help_link/styles/main.less b/x-pack/plugins/ml/public/components/documentation_help_link/styles/main.less deleted file mode 100644 index 6fadf49376ce0ad..000000000000000 --- a/x-pack/plugins/ml/public/components/documentation_help_link/styles/main.less +++ /dev/null @@ -1,5 +0,0 @@ -.documentation-help-link { - i { - margin-left: 4px; - } -} diff --git a/x-pack/plugins/ml/public/components/field_data_card/styles/main.less b/x-pack/plugins/ml/public/components/field_data_card/_field_data_card.scss similarity index 84% rename from x-pack/plugins/ml/public/components/field_data_card/styles/main.less rename to x-pack/plugins/ml/public/components/field_data_card/_field_data_card.scss index 1776afe87fec135..ee125cd392d18ea 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/styles/main.less +++ b/x-pack/plugins/ml/public/components/field_data_card/_field_data_card.scss @@ -1,3 +1,6 @@ +// SASSTODO: This entire sass file needs to be rewritten, using a color blind viz palette and proper vars. +// This will need to be done in a more thorough cleanup. + .ml-field-data-card { width: 360px; height: 435px; @@ -38,12 +41,12 @@ background-color: #bfa180; } + // Use euiPanel styling + @include euiPanel($selector: 'card-contents'); + .card-contents { height: 393px; - border-color: #d9d9d9; - border-style: solid; - border-width: 0px 1px 1px 1px; - border-radius: 0px 0px 5px 5px; + border-radius: 0px 0px $euiBorderRadius $euiBorderRadius; overflow: hidden; } @@ -78,7 +81,7 @@ } .text-code { - font-family: "Lucida Console", Monaco, monospace; + font-family: $euiCodeFontFamily; } .details-select { @@ -93,10 +96,10 @@ svg { font-size: 11px; - font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial; + font-family: $euiFontFamily; text { - fill: #555555; + fill: $euiColorDarkShade; } .info-text { @@ -146,12 +149,11 @@ font-size: 13px; .field-label { + @include euiTextTruncate; + display: inline-block; width: 100px; text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .count-label { diff --git a/x-pack/plugins/ml/public/components/field_data_card/_index.scss b/x-pack/plugins/ml/public/components/field_data_card/_index.scss new file mode 100644 index 000000000000000..c39be8d5f17df2e --- /dev/null +++ b/x-pack/plugins/ml/public/components/field_data_card/_index.scss @@ -0,0 +1 @@ +@import 'field_data_card'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/field_data_card/index.js b/x-pack/plugins/ml/public/components/field_data_card/index.js index 076bb3f5817e394..ab3668f1f1fbc13 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/index.js +++ b/x-pack/plugins/ml/public/components/field_data_card/index.js @@ -10,7 +10,6 @@ import './document_count_chart_directive'; import './field_data_card_directive'; import './metric_distribution_chart_directive'; import './top_values_directive'; -import './styles/main.less'; import 'plugins/ml/components/field_title_bar'; import 'plugins/ml/components/field_type_icon'; import 'plugins/ml/components/chart_tooltip'; diff --git a/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js b/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js index 515ae410a32eea0..e51f635b4c78ee2 100644 --- a/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js +++ b/x-pack/plugins/ml/public/components/field_data_card/metric_distribution_chart_directive.js @@ -255,7 +255,7 @@ module.directive('mlMetricDistributionChart', function () { contents = `${bar.percent}% of documents have
a value of ${minValFormatted}`; } - contents = `
${contents}
`; + contents = `
${contents}
`; if (path.length && path[0].length) { mlChartTooltipService.show(contents, path[0][0], { diff --git a/x-pack/plugins/ml/public/components/field_title_bar/_field_title_bar.scss b/x-pack/plugins/ml/public/components/field_title_bar/_field_title_bar.scss new file mode 100644 index 000000000000000..8df5880d80fe517 --- /dev/null +++ b/x-pack/plugins/ml/public/components/field_title_bar/_field_title_bar.scss @@ -0,0 +1,22 @@ +.ml-field-title-bar { + color: $euiColorEmptyShade; + font-size: $euiFontSizeL; + text-align: center; + border-radius: $euiBorderRadius $euiBorderRadius 0px 0px; + padding: $euiSizeXS $euiSizeS; + + .field-type-icon { + vertical-align: middle; + padding-right: $euiSizeXS; + display: inline-block; + } + + .field-name { + @include euiTextTruncate; + + vertical-align: middle; + padding-right: $euiSizeS; + max-width: 290px; // SASSTODO: Calculate value + display: inline-block; + } +} diff --git a/x-pack/plugins/ml/public/components/field_title_bar/_index.scss b/x-pack/plugins/ml/public/components/field_title_bar/_index.scss new file mode 100644 index 000000000000000..d12cf5d008c448a --- /dev/null +++ b/x-pack/plugins/ml/public/components/field_title_bar/_index.scss @@ -0,0 +1 @@ +@import 'field_title_bar'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/field_title_bar/index.js b/x-pack/plugins/ml/public/components/field_title_bar/index.js index c9a1953aa4bfc2f..0fb644de342fc3e 100644 --- a/x-pack/plugins/ml/public/components/field_title_bar/index.js +++ b/x-pack/plugins/ml/public/components/field_title_bar/index.js @@ -6,5 +6,4 @@ -import './field_title_bar_directive'; -import './styles/main.less'; +import './field_title_bar_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/field_title_bar/styles/main.less b/x-pack/plugins/ml/public/components/field_title_bar/styles/main.less deleted file mode 100644 index 5d7b2b74ed356fd..000000000000000 --- a/x-pack/plugins/ml/public/components/field_title_bar/styles/main.less +++ /dev/null @@ -1,23 +0,0 @@ -.ml-field-title-bar { - color: #ffffff; - font-size: 18px; - text-align: center; - border-radius: 5px 5px 0px 0px; - padding: 5px 6px; - - .field-type-icon { - vertical-align: middle; - padding-right: 4px; - display: inline-block; - } - - .field-name { - vertical-align: middle; - padding-right: 8px; - max-width: 290px; - display: inline-block; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } -} diff --git a/x-pack/plugins/ml/public/components/field_type_icon/styles/main.less b/x-pack/plugins/ml/public/components/field_type_icon/_field_type_icon.scss similarity index 77% rename from x-pack/plugins/ml/public/components/field_type_icon/styles/main.less rename to x-pack/plugins/ml/public/components/field_type_icon/_field_type_icon.scss index 13e8da8ccb0c08e..ba318057bcae19e 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/styles/main.less +++ b/x-pack/plugins/ml/public/components/field_type_icon/_field_type_icon.scss @@ -2,7 +2,7 @@ display: inline !important; .field-type-icon { - padding-right: 2px; + padding-right: $euiSizeXS / 2; display: inline !important; } } diff --git a/x-pack/plugins/ml/public/components/field_type_icon/_index.scss b/x-pack/plugins/ml/public/components/field_type_icon/_index.scss new file mode 100644 index 000000000000000..afd1cb353edb4e2 --- /dev/null +++ b/x-pack/plugins/ml/public/components/field_type_icon/_index.scss @@ -0,0 +1 @@ +@import 'field_type_icon'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/field_type_icon/index.js b/x-pack/plugins/ml/public/components/field_type_icon/index.js index c3ba787e6dd4596..93f2f659b52d516 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/index.js +++ b/x-pack/plugins/ml/public/components/field_type_icon/index.js @@ -7,6 +7,5 @@ import './field_type_icon_directive'; -import './styles/main.less'; export { FieldTypeIcon } from './field_type_icon'; diff --git a/x-pack/plugins/ml/public/components/form_filter_input/styles/main.less b/x-pack/plugins/ml/public/components/form_filter_input/_form_filter_input.scss similarity index 70% rename from x-pack/plugins/ml/public/components/form_filter_input/styles/main.less rename to x-pack/plugins/ml/public/components/form_filter_input/_form_filter_input.scss index a6883e5556e1035..c5056e914b3bfbc 100644 --- a/x-pack/plugins/ml/public/components/form_filter_input/styles/main.less +++ b/x-pack/plugins/ml/public/components/form_filter_input/_form_filter_input.scss @@ -5,27 +5,30 @@ ml-form-filter-input { position: relative; line-height: 0; + // SASSTODO: Make a real selector input[type="text"] { background-color: transparent; max-width: 400px; width: 100%; - padding-right: 30px; + padding-right: $euiSizeXL; } + // SASSTODO: This likely should no be removed input[type="text"]::-ms-clear { display: none; } .ml-filter-input-left-icon { - padding-left: 30px; + padding-left: $euiSizeXL; } + // SASSTODO: Rewrite this selector completely .fa.fa-search { position: absolute; top: 6px; left: 8px; - color: #CCCCCC; + color: $euiColorLightShade; width: 18px; height: 18px; margin-top: 10px; @@ -34,18 +37,19 @@ ml-form-filter-input { line-height: 0; } + // SASSTODO: Rewrite this selector completely .ml-filter-progress-icon { position: absolute; top: 6px; right: 6px; - color: #999DA0; + color: $euiColorMediumShade; width: 18px; height: 18px; margin-top: 10px; margin-right: 0px; a { - color: #CCCCCC; + color: $euiColorLightShade; .fa.fa-times-circle { font-size: 1.4em; @@ -54,7 +58,7 @@ ml-form-filter-input { } a:hover { - color: #999DA0; + color: $euiColorMediumShade; } .fa.fa-spinner { diff --git a/x-pack/plugins/ml/public/components/form_filter_input/_index.scss b/x-pack/plugins/ml/public/components/form_filter_input/_index.scss new file mode 100644 index 000000000000000..9457a23f6de34c7 --- /dev/null +++ b/x-pack/plugins/ml/public/components/form_filter_input/_index.scss @@ -0,0 +1 @@ +@import 'form_filter_input'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/form_filter_input/index.js b/x-pack/plugins/ml/public/components/form_filter_input/index.js index b35d17a2c9c170d..078f7ec320b6a87 100644 --- a/x-pack/plugins/ml/public/components/form_filter_input/index.js +++ b/x-pack/plugins/ml/public/components/form_filter_input/index.js @@ -6,6 +6,5 @@ -import './styles/main.less'; import './form_filter_input_directive'; diff --git a/x-pack/plugins/ml/public/components/form_label/styles/main.less b/x-pack/plugins/ml/public/components/form_label/_form_label.scss similarity index 66% rename from x-pack/plugins/ml/public/components/form_label/styles/main.less rename to x-pack/plugins/ml/public/components/form_label/_form_label.scss index 4965ef14d18b2a1..6cecd46ef3daacc 100644 --- a/x-pack/plugins/ml/public/components/form_label/styles/main.less +++ b/x-pack/plugins/ml/public/components/form_label/_form_label.scss @@ -1,10 +1,12 @@ ml-form-label { display: inline-flex; + // SASSTODO: Apply a real selector span[ml-info-icon] { margin-top: 0px; } + // SASSTODO: Apply a real selector span[ml-info-icon], label { display: block; diff --git a/x-pack/plugins/ml/public/components/form_label/_index.scss b/x-pack/plugins/ml/public/components/form_label/_index.scss new file mode 100644 index 000000000000000..916046a266204cb --- /dev/null +++ b/x-pack/plugins/ml/public/components/form_label/_index.scss @@ -0,0 +1 @@ +@import 'form_label'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/form_label/form_label.js b/x-pack/plugins/ml/public/components/form_label/form_label.js index efdbf6b3926519e..7ba488d3d061ce2 100644 --- a/x-pack/plugins/ml/public/components/form_label/form_label.js +++ b/x-pack/plugins/ml/public/components/form_label/form_label.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './styles/main.less'; - import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/x-pack/plugins/ml/public/components/form_label/index.js b/x-pack/plugins/ml/public/components/form_label/index.js index f850e7b7a2fd2a2..4d4a0f7703a9183 100644 --- a/x-pack/plugins/ml/public/components/form_label/index.js +++ b/x-pack/plugins/ml/public/components/form_label/index.js @@ -6,5 +6,4 @@ -import './form_label_directive'; -import './styles/main.less'; +import './form_label_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/influencers_list/_index.scss b/x-pack/plugins/ml/public/components/influencers_list/_index.scss new file mode 100644 index 000000000000000..90ff743d162f0c5 --- /dev/null +++ b/x-pack/plugins/ml/public/components/influencers_list/_index.scss @@ -0,0 +1 @@ +@import 'influencers_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/influencers_list/_influencers_list.scss b/x-pack/plugins/ml/public/components/influencers_list/_influencers_list.scss new file mode 100644 index 000000000000000..b632b98b75b7c8a --- /dev/null +++ b/x-pack/plugins/ml/public/components/influencers_list/_influencers_list.scss @@ -0,0 +1,106 @@ +.ml-influencers-list { + line-height: 1.45; // SASSTODO: Calc proper value + + .field-label { + font-size: $euiFontSizeXS; + text-align: left; + + .field-value { + @include euiTextTruncate; + + max-width: calc(100% - 34px); // SASSTODO: Calc proper value + display: inline-block; + vertical-align: bottom; + } + } + + .progress { + display:inline-block; + width: calc(100% - 34px); // SASSTODO: Calc porper value + height: 22px; + min-width: 70px; + margin-bottom: 0px; + color: $euiColorDarkShade; + background-color : transparent; + + .progress-bar-holder { + width: calc(100% - 28px); // SASSTODO: Calc porper value + } + + .progress-bar { + height: $euiSizeXS / 2; + margin-top: $euiSizeS; + text-align: right; + line-height: 18px; // SASSTODO: Calc porper value + display: inline-block; + transition: none; + } + } + + // SASSTODO: This range of color is too large, needs to be rewritten and variablized + .progress.critical { + .progress-bar { + background-color: $mlColorCritical; + } + .score-label { + border-color: $mlColorCritical; + } + } + + .progress.major { + .progress-bar { + background-color: $mlColorMajor; + } + .score-label { + border-color: $mlColorMajor; + } + } + + .progress.minor { + .progress-bar { + background-color: $mlColorMinor; + } + .score-label { + border-color: $mlColorMinor; + } + } + + .progress.warning { + .progress-bar { + background-color: $mlColorWarning; + } + .score-label { + border-color: $mlColorWarning; + } + } + + .score-label { + text-align: center; + line-height: 14px; + white-space: nowrap; + font-size: $euiFontSizeXS; + display: inline; + margin-left: $euiSizeXS; + } + + // SASSTODO: Brittle sizing + .total-score-label { + width: $euiSizeXL; + vertical-align: top; + text-align: center; + color: $euiColorDarkShade; + font-size: 11px; + line-height: 14px; + border-radius: $euiBorderRadius; + padding: $euiSizeXS / 2; + margin-top: 1px; + display: inline-block; + border: $euiBorderThin; + } +} + + // SASSTODO: Can .eui-textBreakAll +.ml-influencers-list-tooltip { + word-break: break-all; +} + diff --git a/x-pack/plugins/ml/public/components/influencers_list/index.js b/x-pack/plugins/ml/public/components/influencers_list/index.js index ab50fbb57f37a70..daa351ca90f68c1 100644 --- a/x-pack/plugins/ml/public/components/influencers_list/index.js +++ b/x-pack/plugins/ml/public/components/influencers_list/index.js @@ -5,5 +5,4 @@ */ -import './influencers_list_directive'; -import './styles/main.less'; +import './influencers_list_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/influencers_list/styles/main.less b/x-pack/plugins/ml/public/components/influencers_list/styles/main.less deleted file mode 100644 index d1fd0f14ebb9baf..000000000000000 --- a/x-pack/plugins/ml/public/components/influencers_list/styles/main.less +++ /dev/null @@ -1,104 +0,0 @@ -.ml-influencers-list { - line-height: 1.45; - - .field-label { - font-size: 12px; - text-align: left; - - .field-value { - max-width: calc(~"100% - 34px"); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - display: inline-block; - vertical-align: bottom; - } - } - - .progress { - display:inline-block; - width: calc(~"100% - 34px"); - height: 22px; - min-width: 70px; - margin-bottom: 0px; - color: #555; - background-color : transparent; - - .progress-bar-holder { - width: calc(~"100% - 28px"); - } - - .progress-bar { - height: 2px; - margin-top: 8px; - text-align: right; - line-height: 18px; - display: inline-block; - transition: none; - } - } - - .progress.critical { - .progress-bar { - background-color: #fe5050; - } - .score-label { - border-color: #fe5050; - } - } - - .progress.major { - .progress-bar { - background-color: #fba740; - } - .score-label { - border-color: #fba740; - } - } - - .progress.minor { - .progress-bar { - background-color: #ffdd00; - } - .score-label { - border-color: #ffdd00; - } - } - - .progress.warning { - .progress-bar { - background-color: #8bc8fb; - } - .score-label { - border-color: #8bc8fb; - } - } - - .score-label { - text-align: center; - line-height: 14px; - white-space: nowrap; - font-size: 12px; - display: inline; - margin-left: 4px; - } - - .total-score-label { - width: 32px; - vertical-align: top; - text-align: center; - color: #555; - font-size: 11px; - line-height: 14px; - border-radius: 4px; - padding: 2px; - margin-top: 1px; - display: inline-block; - border: 1px solid #bbbbbb; - } -} - -.ml-influencers-list-tooltip { - word-break: break-all; -} - diff --git a/x-pack/plugins/ml/public/components/item_select/_index.scss b/x-pack/plugins/ml/public/components/item_select/_index.scss new file mode 100644 index 000000000000000..1e899a58e7b1baa --- /dev/null +++ b/x-pack/plugins/ml/public/components/item_select/_index.scss @@ -0,0 +1 @@ +@import 'item_select'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/item_select/_item_select.scss b/x-pack/plugins/ml/public/components/item_select/_item_select.scss new file mode 100644 index 000000000000000..543d9e85eff41c3 --- /dev/null +++ b/x-pack/plugins/ml/public/components/item_select/_item_select.scss @@ -0,0 +1,52 @@ +// This file contains a lot of additive hacks on top of bootstrap. +.ml-item-select { + .ui-select-container { + .ui-select-choices-group-label { + color: $euiColorMediumShade; + } + + small { + font-size: $euiFontSizeXS; + margin-top: $euiSizeXS / 2; + font-style: italic; + color: $euiColorMediumShade; + } + + .ui-select-choices-row.active { + small { + color: $euiColorMediumShade; + } + } + } + + // SASSTODO: Brittle sizing matches other Kibana hacks + .ui-select-multiple.ui-select-bootstrap { + padding: 3px 5px 0px !important; + } +} + +// SASSTODO: This is overwriting core behavior, needs a proper selector +.ui-select-bootstrap.open { + z-index: $euiZComboBox; +} + +.ui-select-container { + .ui-select-choices-group-label { + color: $euiColorMediumShade; + } + + // SASSTODO: Needs to be a proper selector + small { + font-size: $euiFontSizeXS; + margin-top: $euiSizeXS / 2; + font-style: italic; + color: $euiColorDarkShade; + } + + .ui-select-choices-row.active { + // SASSTODO: Needs to be a proper selector + small { + color:$euiColorEmptyShade; + } + } +} diff --git a/x-pack/plugins/ml/public/components/item_select/index.js b/x-pack/plugins/ml/public/components/item_select/index.js index 79e9e94df086d5b..e9e84a6962db072 100644 --- a/x-pack/plugins/ml/public/components/item_select/index.js +++ b/x-pack/plugins/ml/public/components/item_select/index.js @@ -6,5 +6,4 @@ -import './item_select'; -import './styles/main.less'; +import './item_select'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/item_select/styles/main.less b/x-pack/plugins/ml/public/components/item_select/styles/main.less deleted file mode 100644 index 1da45d80785d626..000000000000000 --- a/x-pack/plugins/ml/public/components/item_select/styles/main.less +++ /dev/null @@ -1,47 +0,0 @@ -.ml-item-select { - .ui-select-container { - .ui-select-choices-group-label { - color: #999999; - } - - small { - font-size: 12px; - margin-top: 2px; - font-style: italic; - color: #999999; - } - - .ui-select-choices-row.active { - small { - color: #ffffff; - } - } - } - - .ui-select-multiple.ui-select-bootstrap { - padding: 3px 5px 0px !important; - } -} - -body > .ui-select-bootstrap.open { - z-index: 1050; -} - -body > .ui-select-container { - .ui-select-choices-group-label { - color: #999999; - } - - small { - font-size: 12px; - margin-top: 2px; - font-style: italic; - color: #999999; - } - - .ui-select-choices-row.active { - small { - color: #ffffff; - } - } -} diff --git a/x-pack/plugins/ml/public/components/items_grid/_index.scss b/x-pack/plugins/ml/public/components/items_grid/_index.scss new file mode 100644 index 000000000000000..243c80da5291853 --- /dev/null +++ b/x-pack/plugins/ml/public/components/items_grid/_index.scss @@ -0,0 +1 @@ +@import 'items_grid'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/items_grid/_items_grid.scss b/x-pack/plugins/ml/public/components/items_grid/_items_grid.scss new file mode 100644 index 000000000000000..c3bd6b115bbd68f --- /dev/null +++ b/x-pack/plugins/ml/public/components/items_grid/_items_grid.scss @@ -0,0 +1,3 @@ +.ml-items-grid-page-size-menu { + width: 140px; // SASSTODO: Needs a proper calc +} diff --git a/x-pack/plugins/ml/public/components/items_grid/items_grid.js b/x-pack/plugins/ml/public/components/items_grid/items_grid.js index c653479a125b4d2..e5c9099cc86276d 100644 --- a/x-pack/plugins/ml/public/components/items_grid/items_grid.js +++ b/x-pack/plugins/ml/public/components/items_grid/items_grid.js @@ -22,8 +22,6 @@ import { import { ItemsGridPagination } from './items_grid_pagination'; -import './styles/main.less'; - export function ItemsGrid({ numberColumns, totalItemCount, diff --git a/x-pack/plugins/ml/public/components/items_grid/styles/main.less b/x-pack/plugins/ml/public/components/items_grid/styles/main.less deleted file mode 100644 index f38cd62e98543ef..000000000000000 --- a/x-pack/plugins/ml/public/components/items_grid/styles/main.less +++ /dev/null @@ -1,3 +0,0 @@ -.ml-items-grid-page-size-menu { - width: 140px; -} diff --git a/x-pack/plugins/ml/public/components/job_group_select/_index.scss b/x-pack/plugins/ml/public/components/job_group_select/_index.scss new file mode 100644 index 000000000000000..863a95cdf013be0 --- /dev/null +++ b/x-pack/plugins/ml/public/components/job_group_select/_index.scss @@ -0,0 +1 @@ +@import 'job_group_select'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/job_group_select/styles/main.less b/x-pack/plugins/ml/public/components/job_group_select/_job_group_select.scss similarity index 52% rename from x-pack/plugins/ml/public/components/job_group_select/styles/main.less rename to x-pack/plugins/ml/public/components/job_group_select/_job_group_select.scss index 33ba31bf5b4c236..06d6aa222814c67 100644 --- a/x-pack/plugins/ml/public/components/job_group_select/styles/main.less +++ b/x-pack/plugins/ml/public/components/job_group_select/_job_group_select.scss @@ -1,28 +1,33 @@ +// SASSTODO: More specific selector, this is a bad cascade .ml-job-group-select { .ui-select-container { .ui-select-choices-group-label { - color: #999999; + color: $euiColorMediumShade; } + // SASSTODO: More specific selector small { font-size: 12px; margin-top: 2px; font-style: italic; - color: #999999; + color: $euiColorMediumShade; } .ui-select-choices-row.active { + // SASSTODO: More specific selector small { - color: #ffffff; + color: $euiColorEmptyShade; } } } .ui-select-multiple.ui-select-bootstrap { + // SASSTODO: Needs proper variables padding: 3px 5px 0px !important; } } +// SASSTODO: More specific selector, this is a dangerous overwrite body > .ui-select-bootstrap.open { z-index: 1050; } diff --git a/x-pack/plugins/ml/public/components/job_group_select/index.js b/x-pack/plugins/ml/public/components/job_group_select/index.js index 4c2ab3951f62f37..ae7894b6f29c00b 100644 --- a/x-pack/plugins/ml/public/components/job_group_select/index.js +++ b/x-pack/plugins/ml/public/components/job_group_select/index.js @@ -6,5 +6,4 @@ -import './job_group_select'; -import './styles/main.less'; +import './job_group_select'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/job_select_list/_index.scss b/x-pack/plugins/ml/public/components/job_select_list/_index.scss new file mode 100644 index 000000000000000..800524055a68c77 --- /dev/null +++ b/x-pack/plugins/ml/public/components/job_select_list/_index.scss @@ -0,0 +1 @@ +@import 'job_select_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/job_select_list/styles/main.less b/x-pack/plugins/ml/public/components/job_select_list/_job_select_list.scss similarity index 90% rename from x-pack/plugins/ml/public/components/job_select_list/styles/main.less rename to x-pack/plugins/ml/public/components/job_select_list/_job_select_list.scss index b028336d868e499..271acc16bf4a7f7 100644 --- a/x-pack/plugins/ml/public/components/job_select_list/styles/main.less +++ b/x-pack/plugins/ml/public/components/job_select_list/_job_select_list.scss @@ -1,8 +1,15 @@ +// SASSTODO: This file needs a rewrite. Needs to be variablized and have actual calculations rather than pixels. +// Lots of custom colors in here need to be replaced. + +// SASSTODO: EXTREMELY DANGEROUS OVERWRITE +// Little worried about touching it now navbar { padding: 10px; background-color: #EFF0F1; } +// SASSTODO: EXTREMELY DANGEROUS OVERWRITE +// Little worried about touching it now navbar button[disabled] { color: #FFF; background-color: #0079a5; @@ -36,15 +43,18 @@ navbar button[disabled] { } } + // SASSTODO: Replace with proper selector hr { margin: 2px 0px; } + // SASSTODO: Replace with proper selector label { margin-bottom: 0px; font-weight: normal; } + // SASSTODO: Replace with proper selector input { margin-right: 4px; margin-left: 4px; @@ -55,6 +65,8 @@ navbar button[disabled] { position: absolute; right: 16px; bottom: 12px; + + // SASSTODO: Replace with proper selector input { margin-right: 0px; } @@ -114,6 +126,7 @@ navbar button[disabled] { padding: 0px; line-height: 20px; + // SASSTODO: Replace with proper selector & > label > div { width: 280px; display: inline-block; @@ -128,6 +141,8 @@ navbar button[disabled] { color: #cccccc; } } + + // SASSTODO: Replace with proper selector & > label > div:nth-child(2) { width: 310px; border-left: 1px solid #eee; @@ -158,6 +173,7 @@ navbar button[disabled] { border-radius: 2px; } + // SASSTODO: This needs to be rebuilt .gant-bar-running { background-image:-webkit-gradient(linear, 0 100%, 100% 0, @@ -218,6 +234,7 @@ navbar button[disabled] { margin-bottom: -16px; padding-top: 9px; + // SASSTODO: Needs a poper selector div { height: 1px; border-top: 1px dashed #d6d6d6; @@ -226,6 +243,8 @@ navbar button[disabled] { } .job-row:hover { .job-row-inner { + + // SASSTODO: Needs a poper selector & > label > div { background-color: #f2f2f2; } diff --git a/x-pack/plugins/ml/public/components/job_select_list/index.js b/x-pack/plugins/ml/public/components/job_select_list/index.js index f2cdc43dcc01ee9..b826021fe85f13e 100644 --- a/x-pack/plugins/ml/public/components/job_select_list/index.js +++ b/x-pack/plugins/ml/public/components/job_select_list/index.js @@ -9,5 +9,4 @@ import './job_select_list_directive'; import './job_select_button_directive.js'; -import './job_select_service.js'; -import './styles/main.less'; +import './job_select_service.js'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/json_tooltip/_index.scss b/x-pack/plugins/ml/public/components/json_tooltip/_index.scss new file mode 100644 index 000000000000000..5c2fb69a4d8428d --- /dev/null +++ b/x-pack/plugins/ml/public/components/json_tooltip/_index.scss @@ -0,0 +1 @@ +@import 'json_tooltip'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/json_tooltip/styles/main.less b/x-pack/plugins/ml/public/components/json_tooltip/_json_tooltip.scss similarity index 50% rename from x-pack/plugins/ml/public/components/json_tooltip/styles/main.less rename to x-pack/plugins/ml/public/components/json_tooltip/_json_tooltip.scss index 3e913533d458cdf..af0384240582e7e 100644 --- a/x-pack/plugins/ml/public/components/json_tooltip/styles/main.less +++ b/x-pack/plugins/ml/public/components/json_tooltip/_json_tooltip.scss @@ -1,8 +1,9 @@ .ml-info-icon { - color: #888; - margin: 0 4px; - transition: color 0.15s; + color: $euiColorMediumShade; + margin: 0 $euiSizeXS; + transition: color 0.15s; // SASSTODO: Variablize + // SASSTODO: This needs to be removed /* hard-coded euiIcon size because EuiIconTip doesn't pass on the size attribute to EuiIcon */ .euiIcon { width: 12px; @@ -15,6 +16,6 @@ } .ml-info-icon:hover { - color: #444; - transition: color 0.15s 0.15s; -} + color: $euiColorDarkestShade; + transition: color 0.15s 0.15s; // SASSTODO: Variablize +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js index 7eb971239817698..3b49c39161b0f60 100644 --- a/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js +++ b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js @@ -9,8 +9,6 @@ // component for placing an icon with a popover tooltip anywhere on a page // the id will match an entry in tooltips.json import tooltips from './tooltips.json'; -import './styles/main.less'; - import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/components/loading_indicator/_index.scss b/x-pack/plugins/ml/public/components/loading_indicator/_index.scss new file mode 100644 index 000000000000000..aaab50ac5763f3d --- /dev/null +++ b/x-pack/plugins/ml/public/components/loading_indicator/_index.scss @@ -0,0 +1 @@ +@import 'loading_indicator'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/loading_indicator/styles/main.less b/x-pack/plugins/ml/public/components/loading_indicator/_loading_indicator.scss similarity index 88% rename from x-pack/plugins/ml/public/components/loading_indicator/styles/main.less rename to x-pack/plugins/ml/public/components/loading_indicator/_loading_indicator.scss index addb8a931248b3c..7bbe1d18987b2a7 100644 --- a/x-pack/plugins/ml/public/components/loading_indicator/styles/main.less +++ b/x-pack/plugins/ml/public/components/loading_indicator/_loading_indicator.scss @@ -1,3 +1,5 @@ +// SASSTODO: This needs to be replaced with EuiLoadingSpinner + /* angular */ ml-loading-indicator { .loading-indicator { diff --git a/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js b/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js index a363771c77b6946..42d0e7e8f4c4b15 100644 --- a/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js +++ b/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js @@ -6,8 +6,6 @@ -import './styles/main.less'; - import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js b/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js index b0d34178a00cb32..e607b8a9af64f0b 100644 --- a/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js +++ b/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js @@ -5,8 +5,6 @@ */ - -import './styles/main.less'; import template from './loading_indicator.html'; import { uiModules } from 'ui/modules'; diff --git a/x-pack/plugins/ml/public/components/messagebar/_index.scss b/x-pack/plugins/ml/public/components/messagebar/_index.scss new file mode 100644 index 000000000000000..55e87d264046536 --- /dev/null +++ b/x-pack/plugins/ml/public/components/messagebar/_index.scss @@ -0,0 +1 @@ +@import 'messagebar'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/messagebar/_messagebar.scss b/x-pack/plugins/ml/public/components/messagebar/_messagebar.scss new file mode 100644 index 000000000000000..a4d48ff64ad6b43 --- /dev/null +++ b/x-pack/plugins/ml/public/components/messagebar/_messagebar.scss @@ -0,0 +1,35 @@ + +ml-message-bar { + font-size: $euiFontSizeS; + + // SASSTODO: Needs proper selector + div { + padding: $euiSizeXS; + } + + .ml-message { + border-bottom: 1px solid $euiColorEmptyShade; + color: $euiColorEmptyShade; + margin: 0px; + border-radius: $euiBorderRadius; + padding: $euiSizeXS $euiSizeS; + + // SASSTODO: Needs proper selector + a { + color: $euiColorEmptyShade; + float: right; + } + } + + // SASSTODO: Needs proper variables + .ml-message-info { + background-color: #858585; + } + .ml-message-warning { + background-color: #FF7800; + } + .ml-message-error { + background-color: #e74c3c; + } + +} diff --git a/x-pack/plugins/ml/public/components/messagebar/messagebar.js b/x-pack/plugins/ml/public/components/messagebar/messagebar.js index ade1f5da19d7b96..293b0bc507b2952 100644 --- a/x-pack/plugins/ml/public/components/messagebar/messagebar.js +++ b/x-pack/plugins/ml/public/components/messagebar/messagebar.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import template from './messagebar.html'; import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; diff --git a/x-pack/plugins/ml/public/components/messagebar/styles/main.less b/x-pack/plugins/ml/public/components/messagebar/styles/main.less deleted file mode 100644 index 2156330a048f340..000000000000000 --- a/x-pack/plugins/ml/public/components/messagebar/styles/main.less +++ /dev/null @@ -1,31 +0,0 @@ - -ml-message-bar { - font-size: 14px; - div { - padding: 5px; - } - - .ml-message { - border-bottom: 1px solid #FFFFFF; - color: #FFFFFF; - margin: 0px; - border-radius: 2px; - padding: 5px 10px 5px 10px; - - a { - color: #FFFFFF; - float: right; - } - } - - .ml-message-info { - background-color: #858585; - } - .ml-message-warning { - background-color: #FF7800; - } - .ml-message-error { - background-color: #e74c3c; - } - -} diff --git a/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js b/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js index 2ead5f96dd404ee..12425b7fa6c5462 100644 --- a/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js +++ b/x-pack/plugins/ml/public/components/nav_menu/nav_menu.js @@ -10,12 +10,13 @@ import _ from 'lodash'; import $ from 'jquery'; import template from './nav_menu.html'; import uiRouter from 'ui/routes'; +import chrome from 'ui/chrome'; import { isFullLicense } from '../../license/check_license'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlNavMenu', function (breadcrumbState, config) { +module.directive('mlNavMenu', function (config) { return { restrict: 'E', transclude: true, @@ -73,7 +74,7 @@ module.directive('mlNavMenu', function (breadcrumbState, config) { scope.breadcrumbs = breadcrumbs.filter(Boolean); config.watch('k7design', (val) => scope.showPluginBreadcrumbs = !val); - breadcrumbState.set(scope.breadcrumbs.map(b => ({ text: b.label, href: b.url }))); + chrome.breadcrumbs.set(scope.breadcrumbs.map(b => ({ text: b.label, href: b.url }))); // when the page loads, focus on the first breadcrumb el.ready(() => { diff --git a/x-pack/plugins/ml/public/components/rule_editor/_index.scss b/x-pack/plugins/ml/public/components/rule_editor/_index.scss new file mode 100644 index 000000000000000..cff43ee98bc0839 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/_index.scss @@ -0,0 +1,3 @@ +@import 'rule_editor'; + +@import 'components/detector_description_list/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/rule_editor/styles/main.less b/x-pack/plugins/ml/public/components/rule_editor/_rule_editor.scss similarity index 58% rename from x-pack/plugins/ml/public/components/rule_editor/styles/main.less rename to x-pack/plugins/ml/public/components/rule_editor/_rule_editor.scss index b3770f9b31c9778..d033a255a3d16f5 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/styles/main.less +++ b/x-pack/plugins/ml/public/components/rule_editor/_rule_editor.scss @@ -1,17 +1,19 @@ +// SASSTODO: This file needs a rewrite. It is overwriting EUI in very dangerous / brittle ways. .ml-rule-editor-flyout { - font-size: 14px; + font-size: $euiFontSizeS; .select-rule-action .rule-detector-description-list { - padding-left: 16px; + padding-left: $euiSize; } .select-rule-action-panel { - padding:10px 0px; + padding: $euiSizeS 0px; + // SASSTODO: Dangerous EUI overwrite .euiDescriptionList { .euiDescriptionList__title { flex-basis: 15%; - padding: 0px 16px; + padding: 0px $euiSize; } .euiDescriptionList__description { @@ -20,34 +22,37 @@ .euiDescriptionList__title:nth-child(1), .euiDescriptionList__description:nth-child(2) { - color: #1a1a1a; - font-weight: 600; - border-bottom : 1px solid #d9d9d9; - padding-bottom: 12px; + color: $euiTitleColor; + font-weight: $euiFontWeightBold; + border-bottom: $euiBorderThin; + padding-bottom: $euiSizeM; } .euiDescriptionList__title:nth-child(3), .euiDescriptionList__description:nth-child(4) { - padding-top: 6px; + padding-top: $euiSizeS; } } + // SASSTODO: Dangerous EUI overwrite .euiDescriptionList.euiDescriptionList--column > * { - margin-top: 5px; + margin-top: $euiSizeXS; } } + // SASSTODO: Dangerous EUI overwrite .scope-enable-checkbox { .euiCheckbox__input[disabled] ~ .euiCheckbox__label { color: inherit; } } + // SASSTODO: Dangerous EUI overwrite .scope-field-checkbox { - margin-right: 2px; + margin-right: $euiSizeXS / 2; .euiCheckbox { - margin-top: 6px; + margin-top: $euiSizeXS; } } @@ -57,10 +62,11 @@ } .scope-edit-filter-link { - line-height: 32px; - font-size: 12px; + line-height: $euiSizeXL; + font-size: $euiFontSizeXS; } + // SASSTODO: Needs proper calculated values .condition-edit-value-field { width: 170px; height: 28px; @@ -76,12 +82,12 @@ .euiExpressionButton__value, .euiExpressionButton__description { - color: #c5c5c5; + color: $euiColorLightShade; } } .text-highlight { - font-weight: bold; + font-weight: $euiFontWeightBold; } } diff --git a/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/styles/main.less b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/_detector_description_list.scss similarity index 73% rename from x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/styles/main.less rename to x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/_detector_description_list.scss index 74deb0de3e5c5c7..7ecfe765c4443b5 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/styles/main.less +++ b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/_detector_description_list.scss @@ -1,3 +1,4 @@ +// SASSTODO: Dangerous EUI overwrites .euiDescriptionList.euiDescriptionList--column.rule-detector-description-list { .euiDescriptionList__title { flex-basis: 15%; @@ -8,6 +9,7 @@ } } +// SASSTODO: Dangerous EUI overwrites .euiDescriptionList.euiDescriptionList--column.rule-detector-description-list > * { - margin-top: 4px; + margin-top: $euiSizeXS; } diff --git a/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/_index.scss b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/_index.scss new file mode 100644 index 000000000000000..2754bc45053dcbe --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/_index.scss @@ -0,0 +1 @@ +@import 'detector_description_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js index 55c63be42fe3211..c8a293e134489bb 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js +++ b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js @@ -19,8 +19,6 @@ import { import { formatValue } from '../../../../formatters/format_value'; -import './styles/main.less'; - export function DetectorDescriptionList({ job, detector, diff --git a/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js index f58a18b6e95b4d8..4f751e98a674998 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js @@ -53,8 +53,6 @@ import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { metadata } from 'ui/metadata'; -import './styles/main.less'; - // metadata.branch corresponds to the version used in documentation links. const docsUrl = `https://www.elastic.co/guide/en/elastic-stack-overview/${metadata.branch}/ml-rules.html`; diff --git a/x-pack/plugins/ml/public/datavisualizer/styles/main.less b/x-pack/plugins/ml/public/datavisualizer/_datavisualizer.scss similarity index 56% rename from x-pack/plugins/ml/public/datavisualizer/styles/main.less rename to x-pack/plugins/ml/public/datavisualizer/_datavisualizer.scss index b5d85f82288260c..8c5e22fca2525a2 100644 --- a/x-pack/plugins/ml/public/datavisualizer/styles/main.less +++ b/x-pack/plugins/ml/public/datavisualizer/_datavisualizer.scss @@ -1,29 +1,24 @@ -@import (reference) "~ui/styles/variables"; - .data-visualizer-container { width: 100%; margin-right: auto; margin-left: auto; - padding: 10px 20px; - background-color: #f5f5f5; - -webkit-box-flex: 1; - -webkit-flex: 1 0 auto; - -ms-flex: 1 0 auto; + padding: $euiSizeS $euiSize; + background-color: $euiColorLightestShade; flex: 1 0 auto; .kuiVerticalRhythm + .kuiVerticalRhythm { - margin-top: 20px; + margin-top: $euiSizeL; } .title-container { - padding-bottom: 10px; - padding-right: 6px; + padding-bottom: $euiSize; + padding-right: $euiSizeS; } .main-container { - width: calc(~"100% - 300px"); + width: calc(100% - 300px); display: inline-block; - padding-right: 10px; + padding-right: $euiSizeS; } .no-sidebar { width: 100%; @@ -36,28 +31,30 @@ } .datavisualizer-panel { - padding: 10px 15px; - background-color: #ffffff; + padding: $euiSizeS $euiSize; + background-color: $euiColorEmptyShade; } .datavisualizer-panel.card-panel { - padding-right: 5px; + padding-right: $euiSizeXS; } .query-bar-form { - padding-top: 5px; + padding-top: $euiSizeXS; } .document-count-container { - padding-top: 10px; + padding-top: $euiSizeS; + // SASSTODO: Make a proper selector label { - font-weight: normal; + font-weight: $euiFontWeightRegular; } + // SASSTODO: Make a proper selector .fa-info-circle { - color: #555; - font-size: 13px; + color: $euiColorDarkShade; + font-size: $euiFontSizeS; } } @@ -75,6 +72,7 @@ margin-left: 10px; } + // SASSTODO: Make a proper selector label { display: inline; } @@ -83,33 +81,33 @@ .field-filter-controls { margin-bottom: 0px; min-width: 400px; - padding-bottom: 30px; - padding-right: 10px; + padding-bottom: $euiSizeXL; + padding-right: $euiSizeS; .type-input { float: left; - margin-right: 5px; + margin-right: $euiSizeXS; } } .field-emphasis { - font-weight: 700; + font-weight: $euiFontWeightBold; } .card-container { display: inline-grid; display: -ms-inline-grid; - padding: 0px 10px 10px 0px; + padding: 0px $euiSize $euiSize 0px; } .create-job-content { - padding-bottom: 5px; + padding-bottom: $euiSizeXS; .synopsisTitle { - font-size: 16px; - font-weight: normal; - color: #0079a5; + font-size: $euiFontSizeM; + font-weight: $euiFontWeightRegular; + color: $euiColorPrimary; } .synopsis:hover { @@ -121,20 +119,21 @@ } .synopsisIcon { - padding-top: 8px; + padding-top: $euiSizeS; } .recognized-job-content { - margin-bottom: 40px; + margin-bottom: $euiSizeXXL; .recognizer-result { - margin-top: 10px; + margin-top: $euiSizeS; margin-bottom: 0px; } } } + // SASSTODO: Make a proper selector p { - margin-bottom: 10px; + margin-bottom: $euiSize; } } diff --git a/x-pack/plugins/ml/public/datavisualizer/_index.scss b/x-pack/plugins/ml/public/datavisualizer/_index.scss index af259ce363a569c..bb9e75eeca96d8e 100644 --- a/x-pack/plugins/ml/public/datavisualizer/_index.scss +++ b/x-pack/plugins/ml/public/datavisualizer/_index.scss @@ -1 +1,2 @@ @import './selector/index'; +@import 'datavisualizer'; diff --git a/x-pack/plugins/ml/public/datavisualizer/index.js b/x-pack/plugins/ml/public/datavisualizer/index.js index 6d5d234989988f3..119ca18812160a6 100644 --- a/x-pack/plugins/ml/public/datavisualizer/index.js +++ b/x-pack/plugins/ml/public/datavisualizer/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './selector'; import './datavisualizer_controller'; import 'plugins/ml/components/data_recognizer'; diff --git a/x-pack/plugins/ml/public/explorer/styles/main.less b/x-pack/plugins/ml/public/explorer/_explorer.scss similarity index 67% rename from x-pack/plugins/ml/public/explorer/styles/main.less rename to x-pack/plugins/ml/public/explorer/_explorer.scss index 462343bfcde3459..62ce1c53c94b551 100644 --- a/x-pack/plugins/ml/public/explorer/styles/main.less +++ b/x-pack/plugins/ml/public/explorer/_explorer.scss @@ -1,9 +1,9 @@ .ml-explorer { width: 100%; display: inline-block; - color: #555; + color: $euiColorDarkShade; - .visualize-error { + .visError { h4 { margin-top: 50px; } @@ -11,141 +11,145 @@ .no-results-container { text-align: center; - font-size: 17px; + font-size: $euiFontSizeL; + + // SASSTODO: Use a proper calc padding-top: 60px; .no-results { - background-color: aliceblue; - border: 1px solid #DDEFFF; - padding: 15px; - border-radius: 4px; + background-color: $euiFocusBackgroundColor; + padding: $euiSize; + border-radius: $euiBorderRadius; display: inline-block; + // SASSTODO: Make a proper selector i { - color: #5a9bd6; - margin-right: 5px; + color: $euiColorPrimary; + margin-right: $euiSizeXS; } + + // SASSTODO: Make a proper selector div:nth-child(2) { - margin-top: 10px; - font-size: 13px; + margin-top: $euiSizeXS; + font-size: $euiFontSizeXS; } } } .results-container { + padding: $euiSize 0px $euiSize 0px; + // SASSTODO: Overwrite of bootstrap .col-xs-12 { - width: calc(~"100% - 30px"); - padding-left: 10px; + width: calc(100% - #{$euiSizeXL}); + padding-left: $euiSize; } - padding: 15px 0px 15px 0px; - .no-influencers-warning { float: left; - padding-top: 2px; - padding-left: 15px; - color: #7b8a8b; - font-size: 14px; + padding-top: $euiSizeXS; + padding-left: $euiSize; + color: $euiColorDarkShade; + font-size: $euiFontSizeS; } .panel-title { - color: #7b8a8b; + color: $euiColorDarkShade; display: inline-block; - padding-bottom: 5px; + padding-bottom: $euiSizeXS; } .panel-sub-title { - color: #7b8a8b; + color: $euiColorDarkShade; display: inline-block; - font-size: 12px; + font-size: $euiFontSizeXS; } } .ml-controls { - padding-bottom: 10px; + padding-bottom: $euiSizeS; + // SASSTODO: Make a proper selector label { - font-size: 12px; - padding: 0px 5px 5px 5px; + font-size: $euiFontSizeXS; + padding: $euiSizeXS; + padding-top: 0; } .kuiButtonGroup { - padding: 0px 5px 0px 0px; + padding: 0px $euiSizeXS 0px 0px; position: relative; display: inline-block; } button.dropdown-toggle { + // SASSTODO: Make a proper calc min-width: 160px; text-align: left; - margin-bottom: 3px; + margin-bottom: $euiSizeXS; + // SASSTODO: Make a proper selector span { - font-size: 13px; + font-size: $euiFontSizeXS; } } + // SASSTODO: Make a proper calc button.dropdown-toggle.dropdown-toggle-narrow { min-width: 60px; } button.dropdown-toggle:hover, button.dropdown-toggle:focus { - color: #444444; + color: $euiColorDarkShade; } .dropdown-menu { - font-size: 13px; + font-size: $euiFontSizeXS; } + // SASSTODO: Make a proper selector .dropdown-menu > li > a { - color: #444444; + color: $euiColorDarkShade; text-decoration: none; } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:active, .dropdown-menu > li > a:focus { - color: #ffffff; + color: $euiColorEmptyShade; box-shadow: none; } } .ml-anomalies-controls { - padding-top: 5px; - - .ml-select-severity.euiComboBox { - // Overrides .euiComboBox z-index to ensure it appears below the job picker. - z-index: 0; - } + padding-top: $euiSizeXS; #show_charts_checkbox_control { - padding-top: 28px; + padding-top: $euiSizeL; } } + // SASSTODO: This entire selector needs to be rewritten. + // It looks extremely brittle with very specific sizing units .ml-explorer-swimlane { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - - padding: 0px 0px 0px 0px; - margin-bottom: 10px; + user-select: none; + padding: 0; + margin-bottom: $euiSizeS; ml-explorer-swimlane { + width: 100%; + height: 250px; + & > .title { - background-color: #9c9fa6; - color: white; - font-weight: bold; + background-color: $euiColorMediumShade; + color: $euiColorEmptyShade; + font-weight: $euiFontWeightBold; margin-top: 0px; - border-radius: 2px; - margin-bottom: 5px; + border-radius: $euiSizeXS/ 2; + margin-bottom: $euiSizeXS; } - - width: 100%; - height: 250px; } ml-explorer-swimlane.ml-dragselect-dragging { @@ -166,7 +170,7 @@ opacity: 1; } .sl-cell-inner.sl-cell-inner-selected { - border-width: 2px; + border-width: $euiSizeXS / 2; } .sl-cell-inner.sl-cell-inner-masked { opacity: 0.2; @@ -195,7 +199,7 @@ cursor: default; i { - color: #777777; + color: $euiColorDarkShade; } } .sl-cell-hover { @@ -238,24 +242,24 @@ } div.cells-container { - border: 1px solid #e6e6e6; + border: $euiBorderThin; border-right: 0px; display: inline-block; height: 30px; vertical-align: middle; - background-color: #FFFFFF; + background-color: $euiColorEmptyShade; .sl-cell { - color: #FFF; + color: $euiColorEmptyShade; cursor: default; display: inline-block; height: 29px; - border-right: 1px solid #e6e6e6; + border-right: $euiBorderThin; vertical-align: top; position: relative; .sl-cell-inner, .sl-cell-inner-dragselect { height: 26px; - margin:1px 1px 1px 1px; + margin: 1px; border-radius: 2px; text-align: center; } @@ -264,12 +268,12 @@ } .sl-cell-inner.sl-cell-inner-selected, .sl-cell-inner-dragselect.sl-cell-inner-selected { - border: 2px solid #6B6B6B; + border: 2px solid $euiColorMediumShade; } .sl-cell-inner.sl-cell-inner-selected.sl-cell-inner-masked, .sl-cell-inner-dragselect.sl-cell-inner-selected.sl-cell-inner-masked { - border: 2px solid #000000; + border: 2px solid $euiColorFullShade; opacity: 0.4; } } @@ -282,28 +286,26 @@ .sl-cell.ds-selected { .sl-cell-inner, .sl-cell-inner-dragselect { - border: 2px solid #6B6B6B; + border: 2px solid $euiColorMediumShade; border-radius: 2px; opacity: 1; } } } - } div.lane:last-child { div.cells-container { .sl-cell { - border-bottom: 1px solid #e6e6e6; + border-bottom: $euiBorderThin; } - } } .time-tick-labels { height: 25px; - margin-top: 2px; + margin-top: $euiSizeXS / 2; margin-left: 175px; /* hide d3's domain line */ path.domain { @@ -315,15 +317,15 @@ } /* override d3's default tick styles */ g.tick text { - font-size:11px; - fill: #555; + font-size: 11px; + fill: $euiColorMediumShade; } } } line.gridLine { - stroke: #e6e6e6; - fill : none; + stroke: $euiBorderColor; + fill: none; shape-rendering : crispEdges; stroke-width : 1px; } @@ -333,20 +335,20 @@ } rect.hovered { - stroke: #565656; - stroke-width:2px; + stroke: $euiColorDarkShade; + stroke-width: 2px; } text.laneLabel { font-size: 9pt; - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - fill: #545454; + font-family: $euiFontFamily; + fill: $euiColorDarkShade; } text.timeLabel { font-size: 8pt; - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - fill: #7a7a7a; + font-family: $euiFontFamily; + fill: $euiColorDarkShade; } } diff --git a/x-pack/plugins/ml/public/explorer/_index.scss b/x-pack/plugins/ml/public/explorer/_index.scss new file mode 100644 index 000000000000000..4afbcda2f616cdc --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/_index.scss @@ -0,0 +1,2 @@ +@import 'explorer'; +@import 'explorer_charts/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/explorer/explorer.html b/x-pack/plugins/ml/public/explorer/explorer.html index d94296e3ad6a40e..baea7a6b9011301 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.html +++ b/x-pack/plugins/ml/public/explorer/explorer.html @@ -91,7 +91,7 @@
-
+

No {{swimlaneViewByFieldName}} influencers found

diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less b/x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_chart.scss similarity index 79% rename from x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less rename to x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_chart.scss index ab7452a3c07960e..4808d47fccd4f4b 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_chart.scss @@ -2,26 +2,26 @@ overflow: hidden; .ml-explorer-chart-svg { - font-size: 12px; - font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial; + font-size: $euiSizeXS; + font-family: $euiFontFamily; .line-chart { rect { - fill: #FFFFFF; + fill: $euiColorEmptyShade; opacity: 1; } rect.selected-interval { fill: rgba(200, 200, 200, 0.1); - stroke: #6b6b6b; - stroke-width: 2px; + stroke: $euiColorDarkShade; + stroke-width: $euiSizeXS / 2; stroke-opacity: 0.8; } rect.scheduled-event-marker { stroke-width: 1px; - stroke: #999999; - fill: #cccccc; + stroke: $euiColorMediumShade; + fill: $euiColorLightShade; pointer-events: none; } } @@ -29,7 +29,7 @@ .axis path, .axis line { fill: none; - stroke: #cccccc; + stroke: $euiBorderColor; shape-rendering: crispEdges; } @@ -38,7 +38,7 @@ } .axis text { - fill: #888; + fill: $euiColorDarkShade; } .axis .tick line { @@ -85,19 +85,19 @@ } .anomaly-marker.critical { - fill: #fe5050; + fill: $mlColorCritical; } .anomaly-marker.major { - fill: #ff7e00; + fill: $mlColorMajor; } .anomaly-marker.minor { - fill: #ffdd00; + fill: $mlColorMinor; } .anomaly-marker.warning { - fill: #8bc8fb; + fill: $mlColorWarning; } .anomaly-marker.low { diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_info_tooltip.less b/x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_chart_tooltip.scss similarity index 75% rename from x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_info_tooltip.less rename to x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_chart_tooltip.scss index 0139ae12594788c..d291ff3d3cade16 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_info_tooltip.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_chart_tooltip.scss @@ -3,7 +3,7 @@ } .ml-explorer-chart-description { - font-size: 12px; + font-size: $euiFontSizeXS; font-style: italic; } @@ -16,18 +16,18 @@ grid-template-columns: max-content auto; .mlDescriptionList__title { - color: #fff; - font-size: 12px; + color: $euiColorGhost; + font-size: $euiFontSizeXS; font-weight: normal; white-space: nowrap; grid-column-start: 1; } .mlDescriptionList__description { - color: #fff; - font-size: 12px; + color: $euiColorGhost; + font-size: $euiFontSizeXS; font-weight: bold; - padding-left: 8px; + padding-left: $euiSizeS; max-width: 256px; grid-column-start: 2; } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less b/x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_charts_container.scss similarity index 81% rename from x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less rename to x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_charts_container.scss index 36f23bff87bf414..e2bc6973b278d5f 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/_explorer_charts_container.scss @@ -1,3 +1,4 @@ +// SASSTODO: There is a lot of very specific sizing in here that look brittle to touch .explorer-charts { ml-explorer-charts-container { @@ -7,12 +8,12 @@ } label { - font-size: 12px; + font-size: $euiFontSizeXS; } select { - font-size: 13px; - font-style: normal; + font-size: $euiFontSizeS; + font-style: $euiFontWeightRegular; } .chart-controls { @@ -41,12 +42,12 @@ } .dropdown-menu > li > a { - color: #444444; + color: $euiTextColor; text-decoration: none; } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:active { - color: #ffffff; + color: $euiColorEmptyShade; } button.dropdown-toggle { @@ -55,17 +56,17 @@ margin-bottom: 3px; span { - font-size: 13px; + font-size: $euiFontSizeS; } } button.dropdown-toggle:hover, button.dropdown-toggle:focus { - color: #444444; + color: $euiTextColor; } .ml-anomaly-interim-result { - font-style:italic; + font-style: italic; padding-bottom: 3px; } @@ -73,7 +74,7 @@ width: 100%; padding: 5px 20px; overflow: hidden; - font-size: 12px; + font-size: $euiFontSizeXS; td { padding: 0px 0px 2px 0px; @@ -104,6 +105,7 @@ /* wrapper class for the top right alert icon and view button */ .ml-explorer-chart-icons { + float:right; padding-left: 5px; /* counter-margin for EuiButtonEmpty's padding */ margin: 2px -8px 0 0; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/_index.scss b/x-pack/plugins/ml/public/explorer/explorer_charts/_index.scss new file mode 100644 index 000000000000000..dfa03861e32f364 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/_index.scss @@ -0,0 +1,4 @@ +@import 'components/explorer_chart_label/index'; +@import 'explorer_chart'; +@import 'explorer_chart_tooltip'; +@import 'explorer_charts_container'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss similarity index 100% rename from x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less rename to x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label_badge.less b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss similarity index 98% rename from x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label_badge.less rename to x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss index 80a8faa3af65e36..b540efd6c77f763 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label_badge.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss @@ -1,4 +1,4 @@ -/* +/* Resets the badge's default strong font-weight so it's possible to put custom emphasis inside the badge only on a part of it. Used in the Explorer Chart label badge to display an entity's diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_index.scss b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_index.scss new file mode 100644 index 000000000000000..d112b834f09ad95 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_index.scss @@ -0,0 +1,2 @@ +@import 'explorer_chart_label'; +@import 'explorer_chart_label_badge'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js index b62c219edbfc7c9..bea2a69329d81fd 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './styles/explorer_chart_label.less'; - import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js index 133005c2226f4d7..c33dc2e8fa16881 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './styles/explorer_chart_label_badge.less'; - import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 2ab0d9ccf4b05a9..a443f7278452729 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -9,8 +9,6 @@ * the Machine Learning Explorer dashboard. */ -import './styles/explorer_chart.less'; - import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js index 5cd4551f68af2f0..8c937767e215fce 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -5,8 +5,6 @@ */ -import './styles/explorer_chart_info_tooltip.less'; - import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js index f6212e766b79a59..359c28d58bddb69 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js @@ -9,8 +9,6 @@ * the Machine Learning Explorer dashboard. */ -import './styles/explorer_chart.less'; - import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js index 2a905827c1c4312..6fc44b854c0780e 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js @@ -11,8 +11,6 @@ * anomalies in the raw data in the Machine Learning Explorer dashboard. */ -import './styles/explorer_charts_container.less'; - import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/x-pack/plugins/ml/public/explorer/index.js b/x-pack/plugins/ml/public/explorer/index.js index 3fe0b96bf2900ff..372e46fdb120c8e 100644 --- a/x-pack/plugins/ml/public/explorer/index.js +++ b/x-pack/plugins/ml/public/explorer/index.js @@ -9,7 +9,6 @@ import 'plugins/ml/explorer/explorer_controller'; import 'plugins/ml/explorer/explorer_dashboard_service'; import 'plugins/ml/explorer/explorer_swimlane_directive'; -import 'plugins/ml/explorer/styles/main.less'; import 'plugins/ml/explorer/explorer_charts'; import 'plugins/ml/explorer/select_limit'; import 'plugins/ml/components/job_select_list'; diff --git a/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.js b/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.js index 38405b2c9fd8b52..25f60a37cc103a3 100644 --- a/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.js +++ b/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.js @@ -41,7 +41,7 @@ export class Overrides extends Component { this.state = {}; } - static getDerivedStateFromProps(props) { + static getDerivedStateFromProps(props, state) { const { originalSettings } = props; const { @@ -67,7 +67,7 @@ export class Overrides extends Component { originalColumnNames } = getColumnNames(columnNames, originalSettings); - return { + const initialState = { charset: (charset === undefined) ? originalSettings.charset : charset, format: (format === undefined) ? originalSettings.format : format, hasHeaderRow: (hasHeaderRow === undefined) ? originalSettings.hasHeaderRow : hasHeaderRow, @@ -81,6 +81,8 @@ export class Overrides extends Component { timestampFormat: (timestampFormat === undefined) ? originalSettings.timestampFormat : timestampFormat, timestampField: (timestampField === undefined) ? originalSettings.timestampField : timestampField, }; + + return { ...initialState, ...state }; } componentDidMount() { diff --git a/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.test.js b/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.test.js index a3d368b42f7a735..31d86be680f67e6 100644 --- a/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.test.js +++ b/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/overrides.test.js @@ -5,22 +5,26 @@ */ -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import React from 'react'; import { Overrides } from './overrides'; +function getProps() { + return { + setOverrides: () => { }, + overrides: {}, + originalSettings: {}, + defaultSettings: {}, + setApplyOverrides: () => { }, + fields: [], + }; +} + describe('Overrides', () => { test('render overrides', () => { - const props = { - setOverrides: () => {}, - overrides: {}, - originalSettings: {}, - defaultSettings: {}, - setApplyOverrides: () => {}, - fields: [], - }; + const props = getProps(); const component = shallow( @@ -28,4 +32,23 @@ describe('Overrides', () => { expect(component).toMatchSnapshot(); }); + + test('render overrides and trigger a state change', () => { + const FORMAT_1 = 'delimited'; + const FORMAT_2 = 'ndjson'; + + const props = getProps(); + props.overrides.format = FORMAT_1; + + const component = mount( + + ); + + expect(component.state('format')).toEqual(FORMAT_1); + + component.instance().onFormatChange(FORMAT_2); + + expect(component.state('format')).toEqual(FORMAT_2); + + }); }); diff --git a/x-pack/plugins/ml/public/index.scss b/x-pack/plugins/ml/public/index.scss index 455cf50b221838b..db496606f02ac7f 100644 --- a/x-pack/plugins/ml/public/index.scss +++ b/x-pack/plugins/ml/public/index.scss @@ -5,8 +5,47 @@ @import '@elastic/eui/src/components/panel/variables'; @import '@elastic/eui/src/components/panel/mixins'; -@import '@elastic/eui/src/global_styling/variables/size'; +// ML has it's own variables for coloring +@import 'variables'; -@import 'datavisualizer/index'; -@import 'file_datavisualizer/index'; -@import 'components/nav_menu/index'; +// Protect the rest of Kibana from ML generic namespacing +// SASSTODO: Prefix ml selectors instead +#ml-app { + + // App level + @import 'app'; + + // Sub applications + @import 'datavisualizer/index'; + @import 'explorer/index'; // SASSTODO: This file needs to be rewritten + @import 'file_datavisualizer/index'; + @import 'jobs/index'; // SASSTODO: This collection of sass files has multiple problems + @import 'settings/index'; + @import 'timeseriesexplorer/index'; + + // Components + @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/chart_tooltip/index'; + @import 'components/confirm_modal/index'; + @import 'components/controls/index'; + @import 'components/data_recognizer/index'; + @import 'components/documentation_help_link/index'; + @import 'components/field_data_card/index'; // SASSTODO: This file needs to be rewritten + @import 'components/field_title_bar/index'; + @import 'components/field_type_icon/index'; + @import 'components/form_filter_input/index'; // SASSTODO: This file needs to be rewritten + @import 'components/form_label/index'; + @import 'components/influencers_list/index'; + @import 'components/item_select/index'; // SASSTODO: This file does some dangerous overwrites + @import 'components/items_grid/index'; + @import 'components/job_group_select/index'; // SASSTODO: This file does some dangerous overwrites + @import 'components/job_select_list/index'; // SASSTODO: This file does EXTREMELY DANGEROUS overwrites + @import 'components/json_tooltip/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner + @import 'components/messagebar/index'; + @import 'components/nav_menu/index'; + @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly + + // Hacks are last so they can ovwerite anything above if needed + @import 'hacks'; +} diff --git a/x-pack/plugins/ml/public/jobs/_index.scss b/x-pack/plugins/ml/public/jobs/_index.scss new file mode 100644 index 000000000000000..2f0e5b64ee1a8de --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/_index.scss @@ -0,0 +1,5 @@ +@import 'components/custom_url_editor/index'; +@import 'components/custom_url_editor_old/index'; +@import 'components/job_timepicker_modal/index'; +@import 'jobs_list/index'; // SASSTODO: Various EUI overwrites throughout this folder +@import 'new_job/index'; // SASSTODO: Lots of files need rewrites in here \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/components/custom_url_editor/styles/main.less b/x-pack/plugins/ml/public/jobs/components/custom_url_editor/_custom_url_editor.scss similarity index 58% rename from x-pack/plugins/ml/public/jobs/components/custom_url_editor/styles/main.less rename to x-pack/plugins/ml/public/jobs/components/custom_url_editor/_custom_url_editor.scss index 0a853ce2bfc1e97..ef3ca6b0044e890 100644 --- a/x-pack/plugins/ml/public/jobs/components/custom_url_editor/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/components/custom_url_editor/_custom_url_editor.scss @@ -1,16 +1,19 @@ .ml-edit-url-form { .url-label { + // SASSTODO: Proper calc width: 250px; } .url-link-to-radio { + // SASSTODO: This overwrites EUI .euiRadio { display: inline-block; - padding-right: 20px; + padding-right: $euiSizeL; } } .url-time-range { + // SASSTODO: Proper calc width: 150px; } -} +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/components/custom_url_editor/_index.scss b/x-pack/plugins/ml/public/jobs/components/custom_url_editor/_index.scss new file mode 100644 index 000000000000000..4f1904c9539db68 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/components/custom_url_editor/_index.scss @@ -0,0 +1 @@ +@import 'custom_url_editor'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/components/custom_url_editor/editor.js b/x-pack/plugins/ml/public/jobs/components/custom_url_editor/editor.js index 863887ac8ddb718..b7f729eff23504c 100644 --- a/x-pack/plugins/ml/public/jobs/components/custom_url_editor/editor.js +++ b/x-pack/plugins/ml/public/jobs/components/custom_url_editor/editor.js @@ -31,8 +31,6 @@ import { import { isValidCustomUrlSettingsTimeRange } from '../../../jobs/components/custom_url_editor/utils'; import { isValidLabel } from '../../../util/custom_url_utils'; -import './styles/main.less'; - import { TIME_RANGE_TYPE, URL_TYPE diff --git a/x-pack/plugins/ml/public/jobs/components/custom_url_editor_old/_custom_url_editor.scss b/x-pack/plugins/ml/public/jobs/components/custom_url_editor_old/_custom_url_editor.scss new file mode 100644 index 000000000000000..3bfb728a374cbec --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/components/custom_url_editor_old/_custom_url_editor.scss @@ -0,0 +1,43 @@ +// SASSTODO: Some specific sizing in here looks dangerous to touch +.ml-custom-url-manager { + .add-url-input { + display: block; + } + + select { + width: 100%; + } + + .link-to-label { + padding-right: 20px; + } + + label.disabled { + color: $euiColorLightShade; + } + + .entity-input { + display: inline-block; + margin-bottom: 5px; + } + + .custom-url, .add-url-time-range { + display: flex; + position: relative; + padding-right: 30px; + button.remove-button { + top: 27px; + position: absolute; + right: 6px; + } + + select { + height: $euiSizeXXL; + } + + .form-group { + margin-right: 10px; + } + } + +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/components/custom_url_editor_old/_index.scss b/x-pack/plugins/ml/public/jobs/components/custom_url_editor_old/_index.scss new file mode 100644 index 000000000000000..4f1904c9539db68 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/components/custom_url_editor_old/_index.scss @@ -0,0 +1 @@ +@import 'custom_url_editor'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/_index.scss b/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/_index.scss new file mode 100644 index 000000000000000..6703e43a4e3710f --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/_index.scss @@ -0,0 +1 @@ +@import 'job_timepicker_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/styles/main.less b/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/_job_timepicker_modal.scss similarity index 78% rename from x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/styles/main.less rename to x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/_job_timepicker_modal.scss index 53ee2b22bd73d78..2c8158e27f197a5 100644 --- a/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/_job_timepicker_modal.scss @@ -1,6 +1,6 @@ .job-timepicker-modal { - font-size: 14px; - padding:20px; + font-size: $euiFontSizeS; + padding:$euiSize; cursor: auto; h3 { @@ -14,20 +14,20 @@ } .ml-timepicker-contents { - margin-top: 5px; + margin-top: $euiSizeXS; .btn-info.active, .kuiButton--primary.active { - color: #ffffff; + color: $euiColorGhost; background-color: #154751; border-color: #134049; span { - color: #ffffff; + color: $euiColorGhost; } } .btn-default, .kuiButton--basic { background: transparent; - color: #444444; + color: $euiTextColor; border: 0px; box-shadow: none; text-shadow: none; @@ -50,13 +50,13 @@ padding: 0px 15px; min-width: 294px; width: 294px; - border-left: 1px solid #FFFFFF; - border-right: 1px solid #FFFFFF; + border-left: 1px solid $euiColorGhost; + border-right: 1px solid $euiColorGhost; .ml-timepicker { padding: 13px; padding-top: none; - border: 2px solid #ecf0f1; + border: $euiBorderThick; border-radius: 4px; border-radius: 4px; border-top-left-radius: 0px; @@ -76,10 +76,10 @@ } .ml-timepicker-left-border { - border-left: 1px solid #ecf0f1; + border-left: $euiBorderThin; } .ml-timepicker-right-border { - border-right: 1px solid #ecf0f1; + border-right: $euiBorderThin; } } diff --git a/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/index.js b/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/index.js index 2dea7ffe6dff9e1..b79a888c522432a 100644 --- a/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/index.js +++ b/x-pack/plugins/ml/public/jobs/components/job_timepicker_modal/index.js @@ -8,5 +8,4 @@ import './datafeed_service'; import './job_timepicker_modal_controller'; -import './styles/main.less'; import 'plugins/ml/jobs/new_job/simple/components/watcher'; diff --git a/x-pack/plugins/ml/public/jobs/index.js b/x-pack/plugins/ml/public/jobs/index.js index cedf7d0455d30df..cc1aeb85b8b599c 100644 --- a/x-pack/plugins/ml/public/jobs/index.js +++ b/x-pack/plugins/ml/public/jobs/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './jobs_list'; import './new_job/advanced'; import './new_job/simple/single_metric'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/_index.scss new file mode 100644 index 000000000000000..a215ff2d1a835a2 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/_index.scss @@ -0,0 +1,11 @@ +@import 'jobs_list'; + +@import 'components/edit_job_flyout/index'; +@import 'components/job_details/index'; // SASSTODO: Dangerous EUI overwrites +@import 'components/job_filter_bar/index'; // SASSTODO: Dangerous EUI overwrites +@import 'components/job_group/index'; +@import 'components/jobs_list/index'; // SASSTODO: Dangerous EUI overwrites +@import 'components/jobs_list_view/index'; +@import 'components/jobs_stats_bar/index'; +@import 'components/multi_job_actions/index'; // SASSTODO: Dangerous EUI overwrites +@import 'components/start_datafeed_modal/index'; // SASSTODO: Needs a rewrite \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/_jobs_list.scss b/x-pack/plugins/ml/public/jobs/jobs_list/_jobs_list.scss new file mode 100644 index 000000000000000..d94bb5d67827998 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/_jobs_list.scss @@ -0,0 +1,7 @@ +.job-management { + padding: $euiSizeL; +} + +.new-job-button-container { + float: right; +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/styles/main.less deleted file mode 100644 index e69de29bb2d1d64..000000000000000 diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss similarity index 83% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss index 38343aed2985c40..a625b1274c9b426 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss @@ -2,7 +2,7 @@ .close-editor-button { position: relative; float: right; - top: -5px; + top: -$euiSizeXS; right: 0px; } } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_index.scss new file mode 100644 index 000000000000000..2fdf5a9671be34c --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_index.scss @@ -0,0 +1 @@ +@import 'edit_job_flyout'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 49da9beb51e24f0..734e38844bd732d 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -23,8 +23,6 @@ import { EuiTabbedContent, } from '@elastic/eui'; -import './styles/main.less'; - import { JobDetails, Detectors, Datafeed, CustomUrls } from './tabs'; import { saveJob } from './edit_utils'; import { loadFullJob } from '../utils'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js index ec732c6ae8300de..1a8e36b53909984 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js @@ -37,8 +37,6 @@ import { loadIndexPatterns, } from '../edit_utils'; -import '../styles/main.less'; - const MAX_NUMBER_DASHBOARDS = 1000; const MAX_NUMBER_INDEX_PATTERNS = 1000; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js index 09586793e23f49e..4d1dca27389ada9 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js @@ -18,7 +18,6 @@ import { EuiFieldNumber, } from '@elastic/eui'; -import '../styles/main.less'; import { calculateDatafeedFrequencyDefaultSeconds } from 'plugins/ml/../common/util/job_utils'; import { newJobDefaults } from 'plugins/ml/jobs/new_job/utils/new_job_defaults'; import { parseInterval } from 'plugins/ml/../common/util/parse_interval'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js index 559da710eb8d1db..06fe88fcd8714da 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js @@ -17,7 +17,6 @@ import { EuiSpacer, } from '@elastic/eui'; -import '../styles/main.less'; import { mlJobService } from 'plugins/ml/services/job_service'; import { detectorToString } from 'plugins/ml/util/string_utils'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js index 95cc2c352a8003d..56153c9ff942728 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js @@ -18,7 +18,6 @@ import { EuiComboBox, } from '@elastic/eui'; -import '../styles/main.less'; import { ml } from 'plugins/ml/services/ml_api_service'; export class JobDetails extends Component { diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/_index.scss new file mode 100644 index 000000000000000..ae8b160f3f532f6 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/_index.scss @@ -0,0 +1,2 @@ +@import 'job_details'; +@import 'forecasts_table/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss similarity index 70% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss index 237eee0bb163c1d..f5cb7aef87d9cdc 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss @@ -1,22 +1,29 @@ + .tab-contents { - margin: -8px; - padding: 8px; - background-color: #FFFFFF; + margin: -$euiSizeS; + padding: $euiSizeS; + background-color: $euiColorEmptyShade; + // SASSTODO: Need to remove bootstrap grid .col-md-6:nth-child(1) { + // SASSTODO: Why is this 7? padding-right: 7px; } + + // SASSTODO: Need to remove bootstrap grid .col-md-6:nth-child(2) { + // SASSTODO: Why is this 7? padding-left: 7px; } + // SASSTODO: This needs to be rewriten. Tons of EUI overwrites .job-section { overflow: auto; padding: 5px 15px; background-color: #FBFBFB; border: 1px solid #ecf0f1; - border-radius: 3px; - margin: 4px 0px; + border-radius: $euiBorderRadius; + margin: $euiSizeXS 0px; .euiTable { background-color: transparent; @@ -58,10 +65,12 @@ } } + // SASSTODO: This needs a proper calc .json-textarea { height: 500px; } + // SASSTODO: This needs to be rewritten. A lot of this should be done with the JS props .job-messages-table { max-height: 500px; overflow: auto; @@ -70,7 +79,7 @@ font-size: 12px; th:nth-child(1) { - width: 32px; + width: $euiSizeXL; } th:nth-child(2) { width: 150px; @@ -88,7 +97,7 @@ } .euiTableRowCell { - background-color: #FFFFFF; + background-color: $euiColorEmptyShade; } } } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/_forcasts_table.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/_forcasts_table.scss new file mode 100644 index 000000000000000..137e3a18fb2727f --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/_forcasts_table.scss @@ -0,0 +1,32 @@ +// SASSTODO: This has dangerous EUI overrides and should be removed. Some of it should just use the JS props +.forecasts-table { + .euiTable { + font-size: 12px; + + th:last-child { + width: 60px; + } + + td { + .euiTableCellContent { + padding-top: 4px; + padding-bottom: 4px; + } + } + } + + .view-forecast-btn { + width: 35px; + min-width: 35px; + height: 24px; + + .euiButton__content { + padding: 0px 8px; + } + } + + .euiLink { + font-family: inherit; + } + } + \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/_index.scss new file mode 100644 index 000000000000000..d928a326026b034 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/_index.scss @@ -0,0 +1 @@ +@import 'forcasts_table'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index ed58e884162cd85..157bc9ae719812f 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -22,7 +22,6 @@ import { EuiLink, EuiLoadingSpinner } from '@elastic/eui'; -import './styles/main.less'; import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; import chrome from 'ui/chrome'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/styles/main.less deleted file mode 100644 index f4d77f5f84ff8dc..000000000000000 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/styles/main.less +++ /dev/null @@ -1,30 +0,0 @@ -.forecasts-table { - .euiTable { - font-size: 12px; - - th:last-child { - width: 60px; - } - - td { - .euiTableCellContent { - padding-top: 4px; - padding-bottom: 4px; - } - } - } - - .view-forecast-btn { - width: 35px; - min-width: 35px; - height: 24px; - - .euiButton__content { - padding: 0px 8px; - } - } - - .euiLink { - font-family: inherit; - } -} diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js index 7d6fcff7e42991c..e51d352b10665b0 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js @@ -15,8 +15,6 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; -import './styles/main.less'; - import { extractJobDetails } from './extract_job_details'; import { JsonPane } from './json_tab'; import { DatafeedPreviewPane } from './datafeed_preview_tab'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_index.scss new file mode 100644 index 000000000000000..bd0e4ba6a9d45e6 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_index.scss @@ -0,0 +1 @@ +@import 'job_filter_bar'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss new file mode 100644 index 000000000000000..92c3ae53cbeee00 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss @@ -0,0 +1,23 @@ +.mlJobFilterBar { + // SASSTODO: Dangerou EUI overwrites + .euiFilterGroup { + .euiPopover .euiPanel { + .group-item { + padding: $euiSizeS $euiSize; + } + + .inline-group { + border: 1px solid $euiColorEmptyShade; + border-radius: $euiBorderRadius; + } + + .euiFilterSelectItem:hover, .euiFilterSelectItem:focus { + text-decoration: none; + .inline-group { + border: 1px solid $euiColorDarkShade; + box-shadow: 0px 1px 2px $euiColorMediumShade; + } + } + } + } +} diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js index e2624df92b36824..189af8900df6d88 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js @@ -13,8 +13,6 @@ import React, { import { ml } from 'plugins/ml/services/ml_api_service'; import { JobGroup } from '../job_group'; -import './styles/main.less'; - import { EuiSearchBar, EuiCallOut, @@ -132,6 +130,7 @@ export class JobFilterBar extends Component { }} filters={filters} onChange={this.onChange} + className="mlJobFilterBar" /> { this.renderError() || ''} diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/styles/main.less deleted file mode 100644 index 18259dfd675f16a..000000000000000 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/styles/main.less +++ /dev/null @@ -1,22 +0,0 @@ -.euiFilterGroup { - max-width: 500px; - - .euiPopover .euiPanel { - .group-item { - padding: 6px 12px; - } - - .inline-group { - border: 1px solid #FFFFFF; - border-radius: 3px; - } - - .euiFilterSelectItem:hover, .euiFilterSelectItem:focus { - text-decoration: none; - .inline-group { - border: 1px solid #555555; - box-shadow: 0px 1px 2px #999; - } - } - } -} diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/_index.scss new file mode 100644 index 000000000000000..361f78b08fcc922 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/_index.scss @@ -0,0 +1 @@ +@import 'job_group'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/_job_group.scss similarity index 68% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/_job_group.scss index 333dd78a266c2c2..665f0a746e4a2d2 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/_job_group.scss @@ -1,10 +1,10 @@ .inline-group { font-size: 12px; - background-color: #D9D9D9; + background-color: $euiColorLightShade; padding: 2px 5px; border-radius: 2px; display: inline-block; margin: 0px 3px; - color: #FFFFFF; + color: $euiColorEmptyShade; vertical-align: text-top; } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js index cf60ea1be5791f4..60d9b78bcab09e8 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js @@ -8,8 +8,7 @@ import PropTypes from 'prop-types'; import React from 'react'; -import './styles/main.less'; - +// This should import the colors directly from EUI's palette service rather than be hard coded const COLORS = [ '#00B3A4', // euiColorVis0 '#3185FC', // euiColorVis1 diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/_index.scss new file mode 100644 index 000000000000000..4ab0359d3a50ac5 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/_index.scss @@ -0,0 +1 @@ +@import 'jobs_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/_jobs_list.scss similarity index 84% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/_jobs_list.scss index 18067f7258da4c9..421aa28cb9bf9d1 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/_jobs_list.scss @@ -1,3 +1,4 @@ +// SASSTODO: Rewrite this, can use props for most of it. .jobs-list-table > div > .euiTable { & > thead { th { @@ -71,33 +72,36 @@ white-space: nowrap; } .euiContextMenuItem:last-child:not(.euiContextMenuItem-isDisabled) { - color: #A30000 + color: $euiColorDanger; } } .results-button { - margin-right: 10px; + margin-right: $euiSizeXS; + // SASSTODO: Proper calc width: 18px; } .euiContextMenuItem .euiIcon { - margin-right: 8px; + margin-right: $euiSizeXS; } } .actions-border { + // SASSTODO: Proper calc height: 20px; - border-right: 1px solid #D9D9D9; + border-right: $euiBorderThin; width: 1px; display: inline-block; vertical-align: middle; - margin: 0px 5px; + margin: 0px $euiSizeXS; } .job-description { display: inline-block; } + // SASSTODO: Use eui-textCenter .job-loading-spinner { text-align: center; } @@ -105,7 +109,7 @@ .jobs-list-table.jobs-selected { .results-button { - color: silver; + color: $euiColorLightShade; pointer-events: none; } } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js index 4744d6402c606cd..0165b26095b1636 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -17,7 +17,6 @@ import { toLocaleString } from '../../../../util/string_utils'; import { ResultLinks, actionsMenuContent } from '../job_actions'; import { JobDescription } from './job_description'; import { JobIcon } from '../job_message_icon'; -import './styles/main.less'; import { EuiBasicTable, diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_index.scss new file mode 100644 index 000000000000000..c1d8b70a054a23e --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_index.scss @@ -0,0 +1 @@ +@import 'jobs_list_view'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss similarity index 81% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss index 8ab4345fb99f8c4..64ba582f30c0369 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss @@ -1,12 +1,12 @@ +// SASSTODO: Proper calcs .actions-bar { min-height: 60px; display: flex; + align-items: center; & > div:nth-child(1) { width: 300px; } - & > div:nth-child(2) { - } } .job-management { diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index b6f5971907434fb..76c1dab50e12221 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -5,7 +5,6 @@ */ -import './styles/main.less'; import { timefilter } from 'ui/timefilter'; import { ml } from 'plugins/ml/services/ml_api_service'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/_index.scss new file mode 100644 index 000000000000000..995478bc0966c56 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/_index.scss @@ -0,0 +1 @@ +@import 'jobs_stats_bar'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/_jobs_stats_bar.scss similarity index 53% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/_jobs_stats_bar.scss index 8af32911e05fe3c..63a1bc01c94ae77 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/_jobs_stats_bar.scss @@ -1,12 +1,12 @@ .jobs-stats-bar { - + // SASSTODO: proper calcs height: 42px; padding: 14px; - background-color: #EFF0F1; + background-color: $euiColorLightestShade; .stat { - margin-right: 10px; - .stat-label {} + margin-right: $euiSizeS; + .stat-value { font-weight: bold } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js index 85bb66fca34f617..79cc8bf8ee861c5 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js @@ -5,7 +5,6 @@ */ -import './styles/main.less'; import { JOB_STATE, DATAFEED_STATE } from 'plugins/ml/../common/constants/states'; import PropTypes from 'prop-types'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_index.scss new file mode 100644 index 000000000000000..86d19824f235ad6 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_index.scss @@ -0,0 +1,2 @@ +@import 'multi_job_actions'; +@import 'group_selector/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss similarity index 77% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss index ca42b710d99f62a..e8bd1a45d976663 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss @@ -1,8 +1,10 @@ + // SASSTODO: This looks like it needs some rewriting for all the pixel values .multi-select-actions { padding: 10px 0px; display: inline-block; white-space: nowrap; + // SASSTODO: This looks like it should just be an EUI title .jobs-selected-title { display: inline-block; font-weight: normal; @@ -18,7 +20,7 @@ .actions-border, .actions-border-large { height: 20px; - border-right: 1px solid #D9D9D9; + border-right: $euiBorderThin; width: 1px; display: inline-block; vertical-align: middle; @@ -35,13 +37,14 @@ margin-right: 5px; } + // SASSTODO: Dangerous EUI overwrites .euiContextMenuPanel { .euiContextMenuItem__text { white-space: nowrap; } .euiContextMenuItem:last-child:not(.euiContextMenuItem-isDisabled) { - color: #A30000 + color: $euiColorDanger; } } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss similarity index 60% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss index b6b6af0a27a6d2a..419effffe415578 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss @@ -2,7 +2,6 @@ max-width: 300px; .euiPopoverTitle { - margin-bottom: 8px !important; + margin-bottom: $euiSizeS !important; } -} - +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss new file mode 100644 index 000000000000000..1663397bf21286a --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss @@ -0,0 +1,3 @@ +@import 'group_selector'; +@import 'group_list/index'; +@import 'new_group_input/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss similarity index 55% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss index caab88afdd16912..a0d4ed68fa3230b 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss @@ -1,3 +1,4 @@ + // SASSTODO: proper calcs all through this. Replace shadows .group-list { max-height: 350px; overflow: auto; @@ -5,7 +6,7 @@ .group-item { line-height: 18px; padding: 6px 0px; - border-bottom: 1px solid #eee; + border-bottom: $euiBorderThin; cursor: pointer; &:focus { @@ -14,19 +15,21 @@ } .check { + // SASSTODO: proper calc width: 20px; display: inline-block; } .inline-group { - border: 1px solid #FFFFFF; - border-radius: 3px; + border: 1px solid $euiColorEmptyShade; + border-radius: $euiBorderRadius; } } .group-item:hover { .inline-group { - border: 1px solid #555555; - box-shadow: 0px 1px 2px #999; + border: 1px solid $euiColorDarkShade; + // SASSTODO: Replace with eui shadow mixin + box-shadow: 0px 1px 2px $euiColorMediumShade; } } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss new file mode 100644 index 000000000000000..e1925dbb6d86257 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss @@ -0,0 +1 @@ +@import 'group_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js index b40bf7f8ad0dea9..6006929e8b6ee58 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js @@ -15,8 +15,6 @@ import { keyCodes, } from '@elastic/eui'; -import './styles/main.less'; - import { JobGroup } from '../../../job_group'; function Check({ group, selectedGroups }) { diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index 3afb0dc55a5508d..7e56c30b7df90b2 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -25,7 +25,6 @@ import { import { cloneDeep } from 'lodash'; -import './styles/main.less'; import { ml } from '../../../../../services/ml_api_service'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss new file mode 100644 index 000000000000000..5b05387414fe73e --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss @@ -0,0 +1 @@ +@import 'new_group_input'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss similarity index 95% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss index 9e8f223068093a5..943fc78a9d0faf6 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss @@ -1,3 +1,3 @@ .new-group-input { padding-bottom: 0px; -} +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js index 102544ee49f268d..29167b7fd2200be 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js @@ -19,7 +19,6 @@ import { keyCodes, } from '@elastic/eui'; -import './styles/main.less'; import { validateGroupNames } from '../../../validate_job'; export class NewGroupInput extends Component { diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js index 39e1d7e0154c393..4089b2b3e58815c 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js @@ -7,13 +7,12 @@ import PropTypes from 'prop-types'; import React, { - Component + Component, Fragment } from 'react'; import { ResultLinks } from '../job_actions'; import { MultiJobActionsMenu } from './actions_menu'; import { GroupSelector } from './group_selector'; -import './styles/main.less'; export class MultiJobActions extends Component { constructor(props) { @@ -28,7 +27,7 @@ export class MultiJobActions extends Component { return (
{jobsSelected && - + {this.props.selectedJobs.length} job{s} selected
@@ -45,7 +44,7 @@ export class MultiJobActions extends Component { showDeleteJobModal={this.props.showDeleteJobModal} refreshJobs={this.props.refreshJobs} /> - + }
); diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js index e61dc94fd0b1645..df61ddf6decdbb3 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -10,7 +10,6 @@ import { checkPermission } from 'plugins/ml/privilege/check_privilege'; import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; import React from 'react'; -import './styles/main.less'; import { EuiButton, @@ -28,8 +27,8 @@ export function NewJobButton() { size="s" disabled={(buttonEnabled === false)} fill + iconType="plusInCircle" > - Create new job ); diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/new_job_button/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/new_job_button/styles/main.less deleted file mode 100644 index ebab0b05847e65c..000000000000000 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/new_job_button/styles/main.less +++ /dev/null @@ -1,5 +0,0 @@ -.new-job-button { - margin-right: 3px; -} - - diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/_index.scss new file mode 100644 index 000000000000000..7e753f89ee2f232 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/_index.scss @@ -0,0 +1 @@ +@import 'time_range_selector/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/styles/main.less deleted file mode 100644 index e69de29bb2d1d64..000000000000000 diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss new file mode 100644 index 000000000000000..86b12074fda0a67 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss @@ -0,0 +1 @@ +@import 'time_range_selector'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss similarity index 59% rename from x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/styles/main.less rename to x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss index 5763bcd1c61149a..ac20a6abcc6949f 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss @@ -1,15 +1,16 @@ +// SASSTODO: Looks like this could use a rewrite. Needs selectors .time-range-selector { .time-range-section-container { display: flex; } .time-range-section-title { font-weight: bold; - margin-bottom: 10px; + margin-bottom: $euiSizeS; } .time-range-section { flex: 50%; - padding: 0px 10px; - border-right: 1px solid #D9D9D9; + padding: 0px $euiSizeS; + border-right: $euiBorderThin; } .tab-stack { @@ -21,16 +22,16 @@ float: none; position: relative; display: block; - margin-bottom: 4px; + margin-bottom: $euiSizeXS; & > a { position: relative; display: block; - padding: 10px 15px; - border-radius: 4px; + padding: $euiSizeS $euiSize; + border-radius: $euiSizeXS; } & > a:hover { - background-color: #D9D9D9; + background-color: $euiColorLightestShade; } .body { display: none; @@ -38,8 +39,8 @@ } & > li.active { & > a { - color: #FFF; - background-color: #0079a5; + color: $euiColorEmptyShade; + background-color: $euiColorPrimary; } .body { @@ -48,10 +49,10 @@ } & > li.has-body.active { & > a { - border-radius: 4px 4px 0px 0px; + border-radius: $euiBorderRadius $euiBorderRadius 0px 0px; } .react-datepicker { - border-radius: 0px 0px 4px 4px; + border-radius: 0px 0px $euiBorderRadius $euiBorderRadius; border-top: none; } } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js index 721974343470242..d3b815ac253d558 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js @@ -14,8 +14,6 @@ import { EuiDatePicker, } from '@elastic/eui'; -import './styles/main.less'; - import moment from 'moment'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/_index.scss new file mode 100644 index 000000000000000..d56d10f0cb6a9b4 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/_index.scss @@ -0,0 +1,3 @@ +@import 'advanced/index'; +@import 'simple/index'; +@import 'wizard/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss similarity index 53% rename from x-pack/plugins/ml/public/jobs/new_job/advanced/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss index 308e566f33ec076..1b0c67a44c3afc6 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss @@ -1,46 +1,42 @@ -@import (reference) "~ui/styles/theme"; -@import (reference) '~ui/styles/variables/colors'; -@import (reference) "~ui/styles/variables"; - .ml-new-job { display: block; } +// Required to prevent overflow of flex item in IE11 +.ml-new-job-callout { + width: 100%; +} +// SASSTODO: Proper calcs. This looks too brittle to touch quickly .detector { position: relative; display: inline-block; - background-color: #f8fbff; + background-color: $euiColorLightestShade; padding: 10px; padding-right: 60px; margin-bottom: 5px; border-radius: 3px; - border: 1px solid lightsteelblue; + border: $euiBorderThin; min-width: 360px; & > .button-container { position: absolute; - top: 8px; - right: 8px; - } - - .detector-fields { - // border-bottom: 1px solid lightsteelblue; - // margin-bottom: 4px; - // padding-bottom: 4px; + top: $euiSizeS; + right: $euiSizeS; } .filter-list { - border-bottom: 1px solid lightsteelblue; - margin: 5px 0px; + border-bottom: $euiBorderThin; + margin: $euiSizeXS 0px; + // SASSTODO: Proper calcs .filter { height: 22px; - margin: 4px 0px; + margin: $euiSizeXS 0px; font-style: italic; .button-container { float: right; - margin-left: 10px; + margin-left: $euiSizeS; } } .filter:last-child { @@ -53,6 +49,7 @@ } } +// SASSTODO: Proper calcs .detector-edit-mode { padding-right: 10px; @@ -62,45 +59,48 @@ } .help-pane { - background-color: aliceblue; - border: 1px solid #DDEFFF; - padding: 15px; - border-radius: 3px; + background-color: $euiFocusBackgroundColor; + border: 1px solid darken($euiFocusBackgroundColor, 5%); + padding: $euiSize; + border-radius: $euiSizeXS; } ml-new-job { - font-size: 14px; + font-size: $euiFontSizeS; .ml_json_tab, .ml_data_preview_tab { + + // SASSTODO: Proper calcs .json-textarea { height: 500px; } .json-textarea[readonly] { - background-color: #ffffff; + background-color: $euiColorEmptyShade; } .note { - font-size: 12px; - padding-top: 10px; + font-size: $euiFontSizeXS; + padding-top: $euiSizeS; font-style: italic; - color: @gray6 + color: $euiColorDarkShade; } } i.validation-error { - color: #fe5050; - text-shadow: 1px 1px 1px #BBBBBB; + color: $euiColorDanger; + text-shadow: 1px 1px 1px $euiColorLightestShade; } div.validation-error { - color: #fe5050; - font-size: 12px; + color: $euiColorDanger; + font-size: $euiFontSizeXS; } .tab_contents { - padding-top:20px; + padding-top: $euiSize; + // SASSTODO: Proper calcs .date_container { width: 200px; display: inline-block; @@ -110,6 +110,7 @@ ml-new-job { text-transform: lowercase; } + // SASSTODO: Proper calcs .custom-url, .categorization-filter { display: flex; position: relative; @@ -136,29 +137,34 @@ ml-new-job { } } + // SASSTODO: Proper calcs .influencer-list-container { + @include euiFontSizeXS; + max-height: 500px; overflow: auto; - padding: 10px 15px; - font-size: 13px; - line-height: 1.42857143; - color: #444444; - background-color: #ffffff; + padding: $euiSizeS $euiSize; + color: $euiColorDarkShade; + background-color: $euiColorEmptyShade; background-image: none; - border: 2px solid #ecf0f1; - border-radius: 4px; + border: $euiBorderThick; + border-radius: $euiBorderRadius; } .influencer-list-container { .custom-influencer { - margin-top: 10px; + margin-top: $euiSize; + + // SASSTODO: Proper calcs and selector input[type='text'] { width:200px; float:left; } + + // SASSTODO: Proper selector button { - margin-top:4px; - margin-left:4px; + margin-top: $euiSizeXS; + margin-left: $euiSizeXS; } } @@ -174,25 +180,25 @@ ml-new-job { } .time-example { - color: #999999; - font-size: 12px; - margin-top: 5px; - margin-left: 5px; + color: $euiColorMediumShade; + font-size: $euiFontSizeXS; + margin-top: $euiSizeXS; + margin-left: $euiSizeXS; font-style: italic; } .ml-pre { + @include euiFontSizeXS; + max-height: 500px; overflow: auto; - padding: 5px 15px; - font-size: 13px; - font-family: monospace; - line-height: 1.42857143; - color: #444444; - background-color: #ffffff; + padding: $euiSizeS $euiSize; + font-family: $euiCodeFontFamily; + color: $euiColorDarkShade; + background-color: $euiColorEmptyShade; background-image: none; - border: 2px solid #ecf0f1; - border-radius: 4px; + border: $euiBorderThick; + border-radius: $euiBorderRadius; display: block; unicode-bidi: embed; white-space: pre; diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/advanced/_index.scss new file mode 100644 index 000000000000000..3c0cb16bb836ed0 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/_index.scss @@ -0,0 +1,4 @@ +@import 'advanced'; +@import 'detector_filter_modal/index'; +@import 'detector_modal/index'; +@import 'save_status_modal/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/_detector_filter_modal.scss similarity index 57% rename from x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/_detector_filter_modal.scss index 6f89b34a3b9545d..046151c4a09910e 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/_detector_filter_modal.scss @@ -1,66 +1,76 @@ .detector-filter-modal { - padding:20px; + padding: $euiSizeL; + // SASSTODO: Proper selector h3 { margin-top: 0px; - } + } + small { font-style: italic; } .filter-field-form { - background-color: #FFFFFF; + background-color: $euiColorEmptyShade; border: none; & > div.field-cols { flex: 1 1 1%; - margin-right: 5px; + margin-right: $euiSizeXS; } } .target-container { - border-bottom: 1px solid #CCCCCC; + border-bottom: $euiBorderThin; } .conditions-list-container { + + // SASSTODO: Proper selector .title { - font-weight:bold; + font-weight: $euiFontWeightBold; } - margin-top: 5px; - padding-top: 5px; + margin-top: $euiSizeXS; + padding-top: $euiSizeXS; display: table; border-collapse: collapse; width: 100%; .table-title, .rule-condition { display: table-row; - // border-top: 5px solid transparent; + // SASSTODO: Proper selector & > div { vertical-align: top; display: table-cell; - padding-right: 5px; + padding-right: $euiSizeXS; } + + // SASSTODO: Proper selector & > div:last-child { padding-right: 0px; + button { - margin-top: 3px; + margin-top: $euiSizeXS; } } + div.conditions-connective { display: block; text-transform: lowercase; font-style: italic; } } + + // SASSTODO: Proper selector .title { - border-bottom: 5px solid transparent; + border-bottom: $euiSizeXS solid transparent; } } button.add-new { - margin: 10px 0px; + margin: $euiSizeXS 0px; } } \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/_index.scss new file mode 100644 index 000000000000000..2fe332ea661a165 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/_index.scss @@ -0,0 +1 @@ +@import 'detector_filter_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/index.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/index.js index aa070c000997b89..a39e50c4a955f21 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_filter_modal/index.js @@ -6,5 +6,4 @@ -import './styles/main.less'; import './detector_filter_modal_controller'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/_detector_modal.scss similarity index 54% rename from x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/_detector_modal.scss index a2ab1acf449cea5..ad591dbece9cd94 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/_detector_modal.scss @@ -1,25 +1,27 @@ .detector-modal { - font-size: 14px; - padding:20px; + font-size: $euiFontSizeS; + padding: $euiSizeL; + // SASSTODO: Proper selector h3 { margin-top: 0px; - } + } + small { font-style: italic; } .detector_field_form { - background-color: #FFFFFF; + background-color: $euiColorEmptyShade; border: none; display: flex; & > div.field-cols { flex: 1 1 1%; - margin-right: 5px; + margin-right: $euiSizeXS; select { - -webkit-appearance:none; + -webkit-appearance: none; } } } diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/_index.scss new file mode 100644 index 000000000000000..98518ceba634137 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/_index.scss @@ -0,0 +1 @@ +@import 'detector_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/index.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/index.js index 1c068287b1d9e3b..03ba6183d25b413 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detector_modal/index.js @@ -6,6 +6,5 @@ -import './styles/main.less'; import './detector_modal_controller'; import 'plugins/ml/components/documentation_help_link'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js index 0d1badddc5e164b..b8e35ddce841480 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js @@ -31,6 +31,7 @@ module.directive('mlJobDetectorsList', function ($modal) { fields: '=mlFields', catFieldNameSelected: '=mlCatFieldNameSelected', editMode: '=mlEditMode', + onUpdate: '=mlOnDetectorsUpdate' }, template, controller: function ($scope) { @@ -42,11 +43,14 @@ module.directive('mlJobDetectorsList', function ($modal) { } else { $scope.detectors.push(dtr); } + + $scope.onUpdate(); } }; $scope.removeDetector = function (index) { $scope.detectors.splice(index, 1); + $scope.onUpdate(); }; $scope.editDetector = function (index) { diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout.test.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout.test.js new file mode 100644 index 000000000000000..6c9b597f1ebf26a --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout.test.js @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { EnableModelPlotCallout } from './enable_model_plot_callout_view.js'; + +const message = 'Test message'; + +describe('EnableModelPlotCallout', () => { + + test('Callout is rendered correctly with message', () => { + const wrapper = mount(); + const calloutText = wrapper.find('EuiText'); + + expect(calloutText.text()).toBe(message); + }); + +}); diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_directive.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_directive.js new file mode 100644 index 000000000000000..d1a4b6bb6314f05 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_directive.js @@ -0,0 +1,22 @@ +/* + * 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 'ngreact'; + +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { EnableModelPlotCallout } from './enable_model_plot_callout_view.js'; + +module.directive('mlEnableModelPlotCallout', function (reactDirective) { + return reactDirective( + EnableModelPlotCallout, + undefined, + { restrict: 'E' } + ); +}); diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_view.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_view.js new file mode 100644 index 000000000000000..4b69c0e61fb2e56 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_view.js @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import PropTypes from 'prop-types'; +import React, { Fragment } from 'react'; + +import { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + + +export const EnableModelPlotCallout = ({ message }) => ( + + + + +

+ {message} +

+
+
+
+
+); + +EnableModelPlotCallout.propTypes = { + message: PropTypes.string.isRequired, +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/types/null.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/index.js similarity index 73% rename from x-pack/plugins/canvas/canvas_plugin_src/types/null.js rename to x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/index.js index 27e9cdf59b00467..195c9129e0eea4f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/types/null.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/index.js @@ -4,9 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const nullType = () => ({ - name: 'null', - from: { - '*': () => null, - }, -}); + +import './enable_model_plot_callout_directive.js'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js index 1e03ccde621df18..94cefd62d076f18 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js @@ -6,10 +6,10 @@ -import './styles/main.less'; import './new_job_controller'; import './detectors_list_directive'; import './save_status_modal'; import './field_select_directive'; import 'plugins/ml/components/job_group_select'; import 'plugins/ml/jobs/components/job_timepicker_modal'; +import './enable_model_plot_callout'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html index 59e4ec2bfa62405..89ebace8bc93b05 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html @@ -238,6 +238,7 @@

{{ui.pageTitle}}

ml-fields="fields" ml-cat-field-name-selected="(job.analysis_config.categorization_field_name?true:false)" ml-edit-mode="'NEW'" + ml-on-detectors-update="onDetectorsUpdate" >
{{ ( ui.validation.tabs[1].checks.detectors.message || "At least one detector should be configured" ) }} @@ -275,6 +276,33 @@

{{ui.pageTitle}}

{{ ( ui.validation.tabs[1].checks.influencers.message || "At least one influencer should be selected" ) }}
+ +
+ +
+ +
+ + +
+
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js index 802b88b321c376d..3624c63d3a94815 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js @@ -18,7 +18,12 @@ import { checkFullLicense } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; import template from './new_job.html'; import saveStatusTemplate from 'plugins/ml/jobs/new_job/advanced/save_status_modal/save_status_modal.html'; -import { createSearchItems, createJobForSaving } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; +import { + createSearchItems, + createJobForSaving, + checkCardinalitySuccess, + getMinimalValidJob, +} from 'plugins/ml/jobs/new_job/utils/new_job_utils'; import { loadIndexPatterns, loadCurrentIndexPattern, loadCurrentSavedSearch, timeBasedIndexCheck } from 'plugins/ml/util/index_utils'; import { ML_JOB_FIELD_TYPES, ES_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types'; import { ALLOWED_DATA_UNITS } from 'plugins/ml/../common/constants/validation'; @@ -114,6 +119,8 @@ module.controller('MlNewJob', const mlConfirm = mlConfirmModalService; msgs.clear(); const jobDefaults = newJobDefaults(); + // For keeping a copy of the detectors for comparison + const currentConfigs = { detectors: [], model_plot_config: { enabled: false } }; $scope.job = {}; $scope.mode = MODE.NEW; @@ -156,6 +163,15 @@ module.controller('MlNewJob', $scope.ui.validation.tabs[tab].valid = valid; } }, + cardinalityValidator: { + status: 0, message: '', STATUS: { + FAILED: -1, + NOT_RUNNING: 0, + RUNNING: 1, + FINISHED: 2, + WARNING: 3, + } + }, jsonText: '', changeTab: changeTab, influencers: [], @@ -181,6 +197,7 @@ module.controller('MlNewJob', types: {}, isDatafeed: true, useDedicatedIndex: false, + enableModelPlot: false, modelMemoryLimit: '', modelMemoryLimitDefault: jobDefaults.anomaly_detectors.model_memory_limit, @@ -282,9 +299,37 @@ module.controller('MlNewJob', }); } + function checkForConfigUpdates() { + const { STATUS } = $scope.ui.cardinalityValidator; + // Check if enable model plot was set/has changed and update if it has. + const jobModelPlotValue = $scope.job.model_plot_config ? $scope.job.model_plot_config : { enabled: false }; + const modelPlotSettingsEqual = _.isEqual(currentConfigs.model_plot_config, jobModelPlotValue); + + if (!modelPlotSettingsEqual) { + // Update currentConfigs. + currentConfigs.model_plot_config.enabled = jobModelPlotValue.enabled; + // Update ui portion so checkbox is checked + $scope.ui.enableModelPlot = jobModelPlotValue.enabled; + } + + if ($scope.ui.enableModelPlot === true) { + const unchanged = _.isEqual(currentConfigs.detectors, $scope.job.analysis_config.detectors); + // if detectors changed OR model plot was just toggled on run cardinality + if (!unchanged || !modelPlotSettingsEqual) { + runValidateCardinality(); + } + } else { + $scope.ui.cardinalityValidator.status = STATUS.FINISHED; + $scope.ui.cardinalityValidator.message = ''; + } + } + function changeTab(tab) { $scope.ui.currentTab = tab.index; - if (tab.index === 4) { + // Selecting Analysis Configuration tab + if (tab.index === 1) { + checkForConfigUpdates(); + } else if (tab.index === 4) { createJSONText(); } else if (tab.index === 5) { if ($scope.ui.dataLocation === 'ES') { @@ -651,6 +696,83 @@ module.controller('MlNewJob', } }; + function runValidateCardinality() { + const { STATUS } = $scope.ui.cardinalityValidator; + $scope.ui.cardinalityValidator.status = $scope.ui.cardinalityValidator.STATUS.RUNNING; + + const tempJob = mlJobService.cloneJob($scope.job); + _.merge(tempJob, getMinimalValidJob()); + + ml.validateCardinality(tempJob) + .then((response) => { + const validationResult = checkCardinalitySuccess(response); + + if (validationResult.success === true) { + $scope.ui.cardinalityValidator.status = STATUS.FINISHED; + $scope.ui.cardinalityValidator.message = ''; + } else { + $scope.ui.cardinalityValidator.message = `Creating model plots is resource intensive and not recommended + where the cardinality of the selected fields is greater than 100. Estimated cardinality + for this job is ${validationResult.highCardinality}. + If you enable model plot with this configuration + we recommend you select a dedicated results index on the Job Details tab.`; + + $scope.ui.cardinalityValidator.status = STATUS.WARNING; + } + }) + .catch((error) => { + console.log('Cardinality check error:', error); + $scope.ui.cardinalityValidator.message = `An error occurred validating the configuration + for running the job with model plot enabled. + Creating model plots can be resource intensive and not recommended where the cardinality of the selected fields is high. + You may want to select a dedicated results index on the Job Details tab.`; + + $scope.ui.cardinalityValidator.status = STATUS.FAILED; + }); + } + + $scope.onDetectorsUpdate = function () { + const { STATUS } = $scope.ui.cardinalityValidator; + + if ($scope.ui.enableModelPlot === true) { + // Update currentConfigs since config changed + currentConfigs.detectors = _.cloneDeep($scope.job.analysis_config.detectors); + + if ($scope.job.analysis_config.detectors.length === 0) { + $scope.ui.cardinalityValidator.status = STATUS.FINISHED; + $scope.ui.cardinalityValidator.message = ''; + } else { + runValidateCardinality(); + } + } + }; + + $scope.setModelPlotEnabled = function () { + const { STATUS } = $scope.ui.cardinalityValidator; + + if ($scope.ui.enableModelPlot === true) { + // Start keeping track of the config in case of changes from Edit JSON tab requiring another cardinality check + currentConfigs.detectors = _.cloneDeep($scope.job.analysis_config.detectors); + + $scope.job.model_plot_config = { + enabled: true + }; + + currentConfigs.model_plot_config.enabled = true; + // return early if there's nothing to run a check on yet. + if ($scope.job.analysis_config.detectors.length === 0) { + return; + } + + runValidateCardinality(); + } else { + currentConfigs.model_plot_config.enabled = false; + $scope.ui.cardinalityValidator.status = STATUS.FINISHED; + $scope.ui.cardinalityValidator.message = ''; + delete $scope.job.model_plot_config; + } + }; + // function called by field-select components to set // properties in the analysis_config $scope.setAnalysisConfigProperty = function (value, field) { diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/_index.scss new file mode 100644 index 000000000000000..8bcc808d30b88db --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/_index.scss @@ -0,0 +1 @@ +@import 'save_status_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/_save_status_modal.scss b/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/_save_status_modal.scss new file mode 100644 index 000000000000000..4f8457d2b937acd --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/_save_status_modal.scss @@ -0,0 +1,14 @@ +.save-status-modal { + padding: $euiSizeL; + cursor: auto; + + // SASSTODO: Proper selector + h3 { + margin-top: 0px; + } + + .status-item { + padding-top: $euiSizeS; + font-weight: $euiFontWeightBold; + } +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/index.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/index.js index 2e27445835699ee..8f8c0d3c233c5f8 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/index.js @@ -6,5 +6,4 @@ -import './styles/main.less'; import './save_status_modal_controller'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/styles/main.less deleted file mode 100644 index 005f5a071a5c6a6..000000000000000 --- a/x-pack/plugins/ml/public/jobs/new_job/advanced/save_status_modal/styles/main.less +++ /dev/null @@ -1,13 +0,0 @@ -.save-status-modal { - padding:20px; - cursor: auto; - - h3 { - margin-top: 0px; - } - - .status-item { - padding-top:8px; - font-weight: bold; - } -} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/_index.scss new file mode 100644 index 000000000000000..6cd73f18ec60870 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/_index.scss @@ -0,0 +1,14 @@ +@import 'components/bucket_span_estimator/index'; // SASSTODO: Needs some rewriting +@import 'components/bucket_span_selection/index'; +@import 'components/event_rate_chart/index'; // SASSTODO: Needs some rewriting +@import 'components/fields_selection/index'; // SASSTODO: Needs a rewrite +@import 'components/fields_selection_population/index'; // SASSTODO: Needs a rewrite +@import 'components/general_job_details/index'; // SASSTODO: Needs a rewrite +@import 'components/influencers_selection/index'; +@import 'components/post_save_options/index'; +@import 'components/watcher/index'; // SASSTODO: Needs calc changes + +@import 'multi_metric/index'; // SASSTODO: Needs some rewriting +@import 'population/index'; // SASSTODO: Needs some rewriting +@import 'recognize/index'; // SASSTODO: Needs some rewriting +@import 'single_metric/index'; // SASSTODO: Needs some rewriting diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/_bucket_span_estimator.scss similarity index 69% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/_bucket_span_estimator.scss index eee1a2eb943eccf..484ca68514b95a3 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/_bucket_span_estimator.scss @@ -1,10 +1,11 @@ +// SASSTODO: Proper calcs, this looks to brittle to change .bucket-span-estimator { float: right; margin-right: 5px; margin-top: -27px; button.euiButton.euiButton--small { - font-size: 12px; + font-size: $euiFontSizeS; height: 22px; .euiButton__content { diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/_index.scss new file mode 100644 index 000000000000000..c9c96afec94c1f6 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/_index.scss @@ -0,0 +1 @@ +@import 'bucket_span_estimator'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/index.js index dadd9182f2e1a84..4f56c5de2a6e6a8 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_estimator/index.js @@ -6,5 +6,4 @@ -import './styles/main.less'; import './bucket_span_estimator_directive.js'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/_bucket_span_selector.scss similarity index 97% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/_bucket_span_selector.scss index 2d26d3df00ba1e1..8cb5558e1dbb45c 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/_bucket_span_selector.scss @@ -2,4 +2,4 @@ .bucket-span-input { float: left; } -} +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/_index.scss new file mode 100644 index 000000000000000..d76ab211ee01ab5 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/_index.scss @@ -0,0 +1 @@ +@import 'bucket_span_selector'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/index.js index 295fa48025caf4d..9a1074cfd194b4d 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/bucket_span_selection/index.js @@ -6,5 +6,4 @@ -import './bucket_span_selection_directive'; -import './styles/main.less'; +import './bucket_span_selection_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js index e153c695994bde4..4664b451d29ec1f 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js @@ -9,6 +9,7 @@ import ReactDOM from 'react-dom'; import { EnableModelPlotCheckbox } from './enable_model_plot_checkbox_view.js'; import { ml } from '../../../../../services/ml_api_service'; +import { checkCardinalitySuccess } from '../../../utils/new_job_utils'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); @@ -34,33 +35,12 @@ module.directive('mlEnableModelPlotCheckbox', function () { function errorHandler(error) { console.log('Cardinality could not be validated', error); $scope.ui.cardinalityValidator.status = STATUS.FAILED; - $scope.ui.cardinalityValidator.message = 'Cardinality could not be validated'; - } - - // Only model plot cardinality relevant - // format:[{id:"cardinality_model_plot_high",modelPlotCardinality:11405}, {id:"cardinality_partition_field",fieldName:"clientip"}] - function checkCardinalitySuccess(data) { - const response = { - success: true, - }; - // There were no fields to run cardinality on. - if (Array.isArray(data) && data.length === 0) { - return response; - } - - for (let i = 0; i < data.length; i++) { - if (data[i].id === 'success_cardinality') { - break; - } - - if (data[i].id === 'cardinality_model_plot_high') { - response.success = false; - response.highCardinality = data[i].modelPlotCardinality; - break; - } - } - - return response; + $scope.ui.cardinalityValidator.message = `An error occurred validating the configuration + for running the job with model plot enabled. + Creating model plots can be resource intensive and not recommended where the cardinality of the selected fields is high. + You may want to select a dedicated results index on the Job Details tab.`; + // Go ahead and check the dedicated index box for them + $scope.formConfig.useDedicatedIndex = true; } function validateCardinality() { @@ -131,7 +111,10 @@ module.directive('mlEnableModelPlotCheckbox', function () { $scope.formConfig.enableModelPlot === false) ); const validatorRunning = ($scope.ui.cardinalityValidator.status === STATUS.RUNNING); - const warningStatus = ($scope.ui.cardinalityValidator.status === STATUS.WARNING && $scope.ui.formValid === true); + const warningStatus = ( + ($scope.ui.cardinalityValidator.status === STATUS.WARNING || + $scope.ui.cardinalityValidator.status === STATUS.FAILED) && + $scope.ui.formValid === true); const checkboxText = (validatorRunning) ? 'Validating cardinality...' : 'Enable model plot'; const props = { diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/_event_rate_chart.scss similarity index 71% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/_event_rate_chart.scss index d9bc03fa875780c..fba7df107f02d2a 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/_event_rate_chart.scss @@ -1,26 +1,27 @@ ml-event-rate-chart { svg { - font-size: 12px; - font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial; - margin-top: -20px; + font-size: $euiSizeXS; + font-family: $euiFontFamily; + margin-top: -20px; // SASSTODO: Proper calc } .bar { transition: opacity 0.5s ; - stroke: #FFFFFF; + stroke: $euiColorEmptyShade; } + .bar:hover { opacity: 1; } .axis path, .axis line { fill: none; - stroke: #cccccc; + stroke: $euiBorderColor; shape-rendering: crispEdges; } .axis text { - fill: #000; + fill: $euiTextColor; } .axis .tick line { @@ -33,27 +34,31 @@ ml-event-rate-chart { } .area.bounds { + // SASSTODO: Variabilize fill: rgba(50, 167, 194, 0.25); } .values-line { fill: none; + // SASSTODO: Variabilize stroke: #32a7c2; stroke-width: 2; } .metric-value { opacity: 1; + // SASSTODO: Variabilize fill: #32a7c2; } .context-chart .axis text { + // SASSTODO: Variabilize font-size: 10px; - fill: #333333; + fill: $euiTextColor; } .area.context { - fill: #e4e4e4; + fill: $euiColorLightestShade; } .swimlane .axis text { @@ -77,17 +82,18 @@ ml-event-rate-chart { } .brush .extent { - stroke: #fff; + stroke: $euiColorEmptyShade; fill-opacity: .125; shape-rendering: crispEdges; } + // SASSTODO: Proper calcs .progress { display: inline-block; min-width: 70px; margin-bottom: -5px; margin-left: 49px; - background-color: #FFFFFF; + background-color: $euiColorEmptyShade; height: 2px; border-radius: 0px; } @@ -96,6 +102,7 @@ ml-event-rate-chart { text-align: right; line-height: 18px; transition: none; + // SASSTODO: Variabilize background-color: #32a7c2; } } diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/_index.scss new file mode 100644 index 000000000000000..5f43a677d02ca6b --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/_index.scss @@ -0,0 +1 @@ +@import 'event_rate_chart'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/index.js index 799f818b63b4f2a..cdd3519f6c18501 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/index.js @@ -6,5 +6,4 @@ -import './event_rate_chart_directive'; -import './styles/main.less'; +import './event_rate_chart_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/_fields_selection.scss similarity index 69% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/_fields_selection.scss index 3892ddacf226e6a..9a9fb2b3e0b0952 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/_fields_selection.scss @@ -1,22 +1,23 @@ +// SASSTODO: This has a lot of manual calcs in it. Needs to be rewritten. + .fields-selection { .selection-list-container { max-height: 250px; overflow: auto; - padding: 5px 8px; - font-size: 13px; - line-height: 1.42857143; - color: #444444; - background-color: #ffffff; + padding: $euiSizeXS $euiSizeS; + @include euiFontSizeXS; + color: $euiColorDarkShade; + background-color: $euiColorEmptyShade; background-image: none; - border: 2px solid #ecf0f1; - border-radius: 4px; + border: $euiBorderThick; + border-radius: $euiBorderRadius; .field-row { display: flex; flex-direction: row; label.kuiCheckBoxLabel { - padding-left: 2px !important; + padding-left: $euiSizeXS / 2 !important; flex: auto; white-space: nowrap; overflow: hidden; @@ -28,7 +29,7 @@ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - margin-left: 4px; + margin-left: $euiSizeXS; vertical-align: middle; } } @@ -51,10 +52,10 @@ height: 0; margin-left: 2px; vertical-align: middle; - border-top: 4px dashed; - border-top: 4px solid #000; - border-right: 4px solid transparent; - border-left: 4px solid transparent; + border-top: $euiSizeXS dashed; + border-top: $euiSizeXS solid $euiColorFullShade; + border-right: $euiSizeXS solid transparent; + border-left: $euiSizeXS solid transparent; right: 10px; top: 14px; pointer-events: none; @@ -62,7 +63,7 @@ } } .field-row:first-child { - border-bottom: 1px dashed #CCCCCC; + border-bottom: 1px dashed $euiBorderColor; padding-bottom: 5px; margin-top: 5px; span { diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/_index.scss new file mode 100644 index 000000000000000..91d543ecf37ba4a --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/_index.scss @@ -0,0 +1 @@ +@import 'fields_selection'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/index.js index 4fe2b4b7ac6d6cd..374931319499297 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection/index.js @@ -6,5 +6,4 @@ -import './fields_selection_directive'; -import './styles/main.less'; +import './fields_selection_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/_index.scss new file mode 100644 index 000000000000000..4126423bb855aa0 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/_index.scss @@ -0,0 +1 @@ +@import 'index_selection_population'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/_index_selection_population.scss similarity index 69% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/_index_selection_population.scss index b2fdbe8a6d1ccc2..84fef122864824e 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/_index_selection_population.scss @@ -1,38 +1,41 @@ +// SASSTODO: This has a lot of manual calcs in it. Needs to be rewritten. + .fields-selection-population { .selection-list-container { max-height: 250px; overflow: auto; - padding: 5px 8px; - font-size: 13px; - line-height: 1.42857143; - color: #444444; - background-color: #ffffff; + padding: $euiSizeXS $euiSizeS; + @include euiFontSizeXS; + color: $euiColorDarkShade; + background-color: $euiColorEmptyShade; background-image: none; - border: 2px solid #ecf0f1; - border-radius: 4px; + border: $euiBorderThick; + border-radius: $euiBorderRadius; .field-row { display: flex; flex-direction: row; .field { - padding-left: 2px; + padding-left: $euiSizeXS / 2; flex: auto; white-space: nowrap; overflow: hidden; input { + // SASSTODO: Proper calc min-width: 13px !important; } span { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - margin-left: 4px; + margin-left: $euiSizeXS; } } .agg-type { float: right; + // SASSTODO: Proper calc width: 162px; flex-grow: 0; white-space: nowrap; @@ -55,12 +58,12 @@ position: absolute; width: 0; height: 0; - margin-left: 2px; + margin-left: $euiSizeXS / 2; vertical-align: middle; - border-top: 4px dashed; - border-top: 4px solid #000; - border-right: 4px solid transparent; - border-left: 4px solid transparent; + border-top: $euiSizeXS dashed; + border-top: $euiSizeXS solid $euiColorFullShade; + border-right: $euiSizeXS solid transparent; + border-left: $euiSizeXS solid transparent; right: 43px; top: 14px; pointer-events: none; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/index.js index c6a25a491620e3e..c061df6092cc414 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/fields_selection_population/index.js @@ -7,5 +7,4 @@ import './fields_selection_directive'; -import './styles/main.less'; import 'plugins/ml/components/field_type_icon'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/_general_job_details.scss similarity index 62% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/_general_job_details.scss index 2d7a1bdd47942ba..02f9b519c1c3e3a 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/_general_job_details.scss @@ -1,8 +1,12 @@ +// SASSTODO: This file needs a rewrite. Needs proper calcs + .general-job-details { .advanced-button { min-width: 23px; } .advanced-button-container { + + // SASSTODO: Needs a proper selector label { cursor: pointer; display: inline-block; @@ -10,36 +14,42 @@ } .advanced-group { padding: 10px; - background-color: #F9F9F9; + background-color: $euiColorLightestShade; + + // SASSTODO: Needs a proper selector label { - font-weight: normal; + font-weight: $euiFontWeightRegular; } } } .charts-container { transition: transform 0.2s ; + .chart-list-panel { margin: 0px; padding: 10px; overflow: hidden; + } + .chart-list-panel-population { + padding: 10px; } .card { - background-color: #FFFFFF; - border-radius: 3px; - border: 1px solid #CCCCCC; + background-color: $euiColorEmptyShade; + border-radius: $euiBorderRadius; + border: $euiBorderThin; z-index: 10; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); .card-title { - color: steelblue; - font-weight: bold; + color: $euiColorPrimary; + font-weight: $euiFontWeightBold; margin-left: 10px; margin-top: 1px; margin-bottom: 5px; - border-bottom: 1px solid #CCCCCC; + border-bottom: $euiBorderThin; padding-bottom: 5px; } .chart { @@ -62,43 +72,43 @@ } } .card-behind-0 { - width: calc(~"100% - 32px"); + width: calc(100% - 36px); margin-left: 0px; } .card-behind-1 { - width: calc(~"100% - 44px"); + width: calc(100% - 44px); margin-left: 5px; } .card-behind-2 { - width: calc(~"100% - 53px"); + width: calc(100% - 53px); margin-left: 9.5px; } .card-behind-3 { - width: calc(~"100% - 61px"); + width: calc(100% - 61px); margin-left: 13.5px; } .card-behind-4 { - width: calc(~"100% - 68px"); + width: calc(100% - 68px); margin-left: 17px; } .card-behind-5 { - width: calc(~"100% - 74px"); + width: calc(100% - 74px); margin-left: 20px; } .card-behind-6 { - width: calc(~"100% - 79px"); + width: calc(100% - 79px); margin-left: 22.5px; } .card-behind-7 { - width: calc(~"100% - 83px"); + width: calc(100% - 83px); margin-left: 24.5px; } .card-behind-8 { - width: calc(~"100% - 86px"); + width: calc(100% - 86px); margin-left: 26px; } .card-behind-9 { - width: calc(~"100% - 88px"); + width: calc(100% - 88px); margin-left: 27px; } diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/_index.scss new file mode 100644 index 000000000000000..713563fbe401b49 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/_index.scss @@ -0,0 +1 @@ +@import 'general_job_details'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/index.js index 6eaeb9112acfa95..fee5c486f4566cd 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/general_job_details/index.js @@ -6,5 +6,4 @@ -import './general_job_details_directive'; -import './styles/main.less'; +import './general_job_details_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/_index.scss new file mode 100644 index 000000000000000..4cd9c31f54ac306 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/_index.scss @@ -0,0 +1 @@ +@import 'influencers_selection'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/_influencers_selection.scss similarity index 57% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/_influencers_selection.scss index 8f8ddd417e6c6c4..cdf92313c3dac3d 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/_influencers_selection.scss @@ -1,15 +1,16 @@ .influencers-selection { .ui-select-container[disabled] { - background-color: #D9D9D9 !important; + background-color: $euiColorLightestShade !important; } .ui-select-match-item[disabled]:hover, .ui-select-match-item[disabled]:focus { - background-color: #ffffff; - border-color: #D9D9D9; + background-color: $euiColorEmptyShade; + border-color: $euiBorderColor; } } .default-influencer { font-style: italic; + // SASSTODO: Variabilize color: #acb6c0; } diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/index.js index 1cb4162aedeea54..455261a9db6ec83 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/influencers_selection/index.js @@ -7,5 +7,4 @@ import './influencers_selection_directive'; -import './styles/main.less'; import 'plugins/ml/components/field_type_icon'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/_index.scss new file mode 100644 index 000000000000000..c1241466690e3ce --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/_index.scss @@ -0,0 +1 @@ +@import 'post_save_options'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/_post_save_options.scss similarity index 56% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/_post_save_options.scss index 33fb73fde4e3734..412db3b6b194127 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/_post_save_options.scss @@ -1,5 +1,5 @@ .post-save-options { .disabled { - color: #CCC; + color: $euiColorLightShade; } } diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/index.js index a9c9c8891e955ce..717232daf32a94f 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/post_save_options/index.js @@ -6,6 +6,5 @@ -import './styles/main.less'; import './post_save_options_directive.js'; import 'plugins/ml/jobs/new_job/simple/components/watcher'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/_index.scss new file mode 100644 index 000000000000000..737d80dfb6082fd --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/_index.scss @@ -0,0 +1 @@ +@import 'watcher'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/_watcher.scss similarity index 52% rename from x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/_watcher.scss index ec7cbd09e68bc54..f89673d0ea5c410 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/_watcher.scss @@ -1,13 +1,13 @@ .create-watch { - font-size: 14px; - padding: 10px; + font-size: $euiFontSizeS; + padding: $euiSizeS; .sub-form-group { display: inline-block; - margin-right: 10px; + margin-right: $euiSizeS; .interval { - width: 65px; + width: 65px; // SASSTODO: Proper calc } } @@ -18,18 +18,18 @@ .sub-form-group:first-child { .euiFormControlLayout { display: inline-block; - width: 70px; + width: 70px; // SASSTODO: Proper calc } } .email-section { - padding: 10px; + padding: $euiSizeS; padding-left: 0px; - padding-bottom: 5px + padding-bottom: $euiSizeXS; } .form-group:last-child { - margin-bottom: 0px + margin-bottom: 0px; } .dropdown-group { @@ -42,34 +42,38 @@ cursor: pointer; } button.dropdown-toggle { - width: 120px; + width: 120px; // SASSTODO: Proper calc text-align: left; - margin-bottom: 3px; - + margin-bottom: $euiSizeXS; + + // SASSTODO: Proper calc span { - font-size: 13px; + font-size: $euiFontSizeXS; } } .dropdown-menu { - min-width: 120px; - font-size: 13px; + min-width: 120px; // SASSTODO: Proper calc + font-size: $euiFontSizeXS; + // SASSTODO: Proper selector li > a { - color: #444444; + color: $euiColorDarkShade; text-decoration: none; box-shadow: none; } + + // SASSTODO: Proper selector li > a:hover, li > a:active, li > a:focus { - color: #ffffff; + color: $euiColorEmptyShade; } } .watch-exists-warning { - color: #fe5050; + color: $euiColorWarning; } } .create-watch-embedded { - background-color: #F9F9F9; + background-color: $euiColorLightestShade; } diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js index e5c48efa71721a4..97e62ff2da013b5 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js @@ -6,6 +6,5 @@ -import './styles/main.less'; import './create_watch_directive.js'; import './create_watch_service.js'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/_index.scss new file mode 100644 index 000000000000000..e75c986b33fbce2 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/_index.scss @@ -0,0 +1 @@ +@import 'create_job/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/_create_job.scss old mode 100755 new mode 100644 similarity index 68% rename from x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/_create_job.scss index ac4a6789e3dd7e6..0b85a380ff92173 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/_create_job.scss @@ -1,247 +1,257 @@ -@import (reference) "~ui/styles/variables"; - -.multi-metric-job-container { - font-size: 14px; - width: 100%; - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; - - - .job-state-info { - margin-bottom: 10px; - } - - .form-controls, .charts-container { - margin: 0px; - margin-right: -10px; - - & > div { - border: 1px solid #E4E4E3; - border-top: 0px; - } - - & > h4 { - margin-top: 0px; - margin-bottom: 0px; - padding: 10px; - background-color: @kibanaBlue3; - color: #FFFFFF; - } - - .btn-load-vis { - border-radius: 4px !important; - margin-top: -2px; - } - - .remove-split { - height: 22px; - font-size: 12px; - line-height: 12px; - margin-left: 10px; - min-width: 100px; - } - } - - .charts-container { - margin-left: -10px; - - label { - margin-bottom: 15px; - } - } - - .form-section { - margin: 0px 0px 0px 0px; - border-bottom: 1px solid #E4E4E3; - padding: 10px; - overflow: hidden; - - &.form-section-overflow { - overflow: visible; - - .ui-select-bootstrap .ui-select-toggle > a.btn { - margin-top: 0; - border: 0; - } - } - - h4, h5 { - margin-top: 0px; - display: inline-block; - } - .form-group:last-child { - margin-bottom: 0px; - } - - .help-text { - border: 1px solid #e8e8e8; - padding: 5px; - background-color: #f1f1f1; - border-radius: 3px; - font-size: 12px; - } - } - - .form-section:last-of-type { - margin: 0px; - border-bottom: 0px solid #E4E4E3; - } - - .form-section-collapsed { - height: 46px; - } - - div.validation-error { - color: #fe5050; - font-size: 12px; - } -} - -.chart-container { - .chart-loader { - border: 1px solid #e5e5e5; - margin-left: 49px; - display: flex; - margin-bottom: 25px; - position: relative; - background-color: rgba(255, 255, 255, 0.75); - transition: opacity 0.5s ; - - .status-label { - width: 50%; - text-align: center; - margin: auto; - - .no-results { - color: #555; - - i { - color: #5a9bd6 - } - h2, h4 { - margin-top: 0px; - } - } - } - } - - .event-rate-chart-loader { - margin-left: 0px; - border: none; - } -} - -ml-multi-metric-job-chart { - svg { - font-size: 12px; - font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial; - margin-top: -20px; - } - - .bar { - transition: opacity 0.5s ; - stroke: #FFFFFF; - } - .bar:hover { - opacity: 1; - } - - .axis path, .axis line { - fill: none; - stroke: #cccccc; - shape-rendering: crispEdges; - } - - .axis text { - fill: #000; - } - - .axis .tick line { - stroke: rgba(0, 0, 0, 0.1); - stroke-width: 0px; - } - - .area { - stroke-width: 1; - } - - .area.bounds { - fill: rgba(50, 167, 194, 0.25); - } - - .values-line { - fill: none; - stroke: #32a7c2; - stroke-width: 2; - } - - .values-dots circle { - fill: #32a7c2; - stroke-width: 0; - } - - .metric-value { - opacity: 1; - fill: #32a7c2; - } - - .context-chart .axis text { - font-size: 10px; - fill: #333333; - } - - .area.context { - fill: #e4e4e4; - } - - .swimlane .axis text { - display: none; - } - - .swimlane rect.swimlane-cell-hidden { - display: none; - } - - .bottom-swimlane .axis .tick line { - stroke-width: 1px; - } - - .swimlane-cells { - transition: opacity 0.5s; - } - - .swimlane-cell:hover { - opacity: 1; - } - - .brush .extent { - stroke: #fff; - fill-opacity: .125; - shape-rendering: crispEdges; - } - - .progress { - display: inline-block; - min-width: 70px; - margin-bottom: -5px; - margin-left: 49px; - background-color: #FFFFFF; - height: 2px; - border-radius: 0px; - } - - .progress-bar { - text-align: right; - line-height: 18px; - transition: none; - background-color: #32a7c2; - } -} - -// hide the default es loading indicator bar as it can't be switched off -// for standard es searches using the http header. -.loadingIndicator__bar { - display: none; -} +// SASSTODO: This file needs works with calcs and variables +.multi-metric-job-container { + font-size: $euiFontSizeS; + width: 100%; + margin-right: auto; + margin-left: auto; + padding-left: $euiSize; + padding-right: $euiSize; + + + .job-state-info { + margin-bottom: $euiSizeS; + } + + .form-controls, .charts-container { + margin: 0px; + margin-right: -$euiSizeS; + + // SASSTODO: Proper selector + & > div { + border: 1px solid $euiBorderColor; + border-top: 0px; + } + + // SASSTODO: Proper selector + & > h4 { + margin-top: 0px; + margin-bottom: 0px; + padding: $euiSizeS; + background-color: $euiColorPrimary; + color: $euiColorEmptyShade; + } + + .btn-load-vis { + border-radius: $euiBorderRadius !important; + margin-top: -2px; + } + + // SASSTODO: Proper calcs, too brittle to change + .remove-split { + height: 22px; + font-size: 12px; + line-height: 12px; + margin-left: 10px; + min-width: 100px; + } + } + + .charts-container { + margin-left: -$euiSizeS; + + label { + margin-bottom: $euiSize; + } + } + + .form-section { + margin: 0px 0px 0px 0px; + border-bottom: 1px solid $euiBorderColor; + padding: $euiSizeS; + overflow: hidden; + + &.form-section-overflow { + overflow: visible; + + .ui-select-bootstrap .ui-select-toggle > a.btn { + margin-top: 0; + border: 0; + } + } + + // SASSTODO: Proper selectors + h4, h5 { + margin-top: 0px; + display: inline-block; + } + + .form-group:last-child { + margin-bottom: 0px; + } + + .help-text { + border: 1px solid $euiBorderColor; + padding: $euiSizeXS; + background-color: $euiColorEmptyShade; + border-radius: $euiBorderRadius; + font-size: $euiFontSizeXS; + } + } + + .form-section:last-of-type { + margin: 0px; + border-bottom: 0px solid $euiBorderColor; + } + + // SASSTODO: Proper calcs + .form-section-collapsed { + height: 46px; + } + + div.validation-error { + color: $euiColorDanger; + font-size: $euiFontSizeXS; + } +} + +.chart-container { + // SASSTODO: Proper calcs + .chart-loader { + border: 1px solid $euiBorderColor; + margin-left: 49px; + display: flex; + margin-bottom: 25px; + position: relative; + background-color: rgba(255, 255, 255, 0.75); + transition: opacity 0.5s ; + + .status-label { + width: 50%; + text-align: center; + margin: auto; + + .no-results { + color: $euiColorDarkShade; + + // SASSTODO: Proper selector + i { + color: $euiColorPrimary; + } + + // SASSTODO: Proper selector + h2, h4 { + margin-top: 0px; + } + } + } + } + + .event-rate-chart-loader { + margin-left: 0px; + border: none; + } +} + +ml-multi-metric-job-chart { + svg { + font-size: $euiFontSizeXS; + font-family: $euiFontFamily; + // SASSTODO: Proper calc + margin-top: -20px; + } + + .bar { + transition: opacity 0.5s ; + stroke: $euiColorEmptyShade; + } + .bar:hover { + opacity: 1; + } + + .axis path, .axis line { + fill: none; + stroke: $euiBorderColor; + shape-rendering: crispEdges; + } + + .axis text { + fill: $euiTextColor; + } + + .axis .tick line { + stroke: rgba(0, 0, 0, 0.1); + stroke-width: 0px; + } + + .area { + stroke-width: 1; + } + + .area.bounds { + fill: rgba(50, 167, 194, 0.25); + } + + .values-line { + fill: none; + stroke: #32a7c2; + stroke-width: 2; + } + + .values-dots circle { + fill: #32a7c2; + stroke-width: 0; + } + + .metric-value { + opacity: 1; + fill: #32a7c2; + } + + .context-chart .axis text { + font-size: 10px; + fill: #333333; + } + + .area.context { + fill: #e4e4e4; + } + + .swimlane .axis text { + display: none; + } + + .swimlane rect.swimlane-cell-hidden { + display: none; + } + + .bottom-swimlane .axis .tick line { + stroke-width: 1px; + } + + .swimlane-cells { + transition: opacity 0.5s; + } + + .swimlane-cell:hover { + opacity: 1; + } + + .brush .extent { + stroke: $euiColorEmptyShade; + fill-opacity: .125; + shape-rendering: crispEdges; + } + + .progress { + display: inline-block; + min-width: 70px; + margin-bottom: -5px; + margin-left: 49px; + background-color: #FFFFFF; + height: 2px; + border-radius: 0px; + } + + .progress-bar { + text-align: right; + line-height: 18px; + transition: none; + background-color: #32a7c2; + } +} + +// hide the default es loading indicator bar as it can't be switched off +// for standard es searches using the http header. +.loadingIndicator__bar { + display: none; +} diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/_index.scss new file mode 100644 index 000000000000000..db63a0bf13dfb40 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/_index.scss @@ -0,0 +1 @@ +@import 'create_job'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/index.js index be0543676feda04..93ed828599bded4 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './create_job_controller'; import './create_job_service'; import './create_job_chart_directive'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/population/_index.scss new file mode 100644 index 000000000000000..e75c986b33fbce2 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/_index.scss @@ -0,0 +1 @@ +@import 'create_job/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/_create_job.scss old mode 100755 new mode 100644 similarity index 71% rename from x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/_create_job.scss index d07c23a3b1c9143..4ad173c43243701 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/_create_job.scss @@ -1,285 +1,293 @@ -@import (reference) "~ui/styles/variables"; - -.population-job-container { - font-size: 14px; - width: 100%; - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; - - - .job-state-info { - margin-bottom: 10px; - } - - .form-controls, .charts-container { - margin: 0px; - margin-right: -10px; - - & > div { - border: 1px solid #E4E4E3; - border-top: 0px; - } - - & > h4 { - margin-top: 0px; - margin-bottom: 0px; - padding: 10px; - background-color: @kibanaBlue3; - color: #FFFFFF; - } - - .btn-load-vis { - border-radius: 4px !important; - margin-top: -2px; - } - } - - .charts-container { - margin-left: -10px; - - label { - margin-bottom: 15px; - } - } - - .form-section { - margin: 0px 0px 0px 0px; - border-bottom: 1px solid #E4E4E3; - padding: 10px; - overflow: hidden; - - &.form-section-overflow { - overflow: visible; - - .ui-select-bootstrap .ui-select-toggle > a.btn { - margin-top: 0; - border: 0; - } - } - - h4, h5 { - margin-top: 0px; - display: inline-block; - } - .form-group:last-child { - margin-bottom: 0px; - } - - .help-text { - border: 1px solid #e8e8e8; - padding: 5px; - background-color: #f1f1f1; - border-radius: 3px; - font-size: 12px; - } - } - - .form-section:last-of-type { - margin: 0px; - border-bottom: 0px solid #E4E4E3; - } - - .form-section-collapsed { - height: 46px; - } - - - .charts-container { - - .chart { - label { - display: block; - } - .split-controls { - height: 45px; - - .split-field-container { - width: 255px; - display: inline-block; - .split-field-select { - width: 250px; - display: inline-block; - overflow: hidden; - } - } - .split-align { - vertical-align: top; - margin-top: 7px; - display: inline-block; - } - .remove-split { - height: 22px; - font-size: 12px; - line-height: 12px; - margin-top: 6px; - min-width: 100px; - } - } - } - - .card { - label { - margin-bottom: 10px; - } - } - - .split-select { - width: auto; - } - } - - div.validation-error { - color: #fe5050; - font-size: 12px; - } -} - -.chart-container { - .chart-loader { - border: 1px solid #e5e5e5; - margin-left: 49px; - display: flex; - margin-bottom: 25px; - position: relative; - background-color: rgba(255, 255, 255, 0.75); - transition: opacity 0.5s ; - - .status-label { - width: 50%; - text-align: center; - margin: auto; - - .no-results { - color: #555; - - i { - color: #5a9bd6 - } - h2, h4 { - margin-top: 0px; - } - } - } - } - - .event-rate-chart-loader { - margin-left: 0px; - border: none; - } -} - -ml-population-job-chart { - svg { - font-size: 12px; - font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial; - margin-top: -20px; - } - - .bar { - transition: opacity 0.5s ; - stroke: #FFFFFF; - } - .bar:hover { - opacity: 1; - } - - .axis path, .axis line { - fill: none; - stroke: #cccccc; - shape-rendering: crispEdges; - } - - .axis text { - fill: #000; - } - - .axis .tick line { - stroke: rgba(0, 0, 0, 0.1); - stroke-width: 0px; - } - - .area { - stroke-width: 1; - } - - .area.bounds { - fill: rgba(50, 167, 194, 0.25); - } - - .values-line { - fill: none; - stroke: #32a7c2; - stroke-width: 2; - } - - .values-dots circle { - fill: #32a7c2; - stroke-width: 0; - fill-opacity: 0.5; - } - - .metric-value { - opacity: 1; - fill: #32a7c2; - } - - .context-chart .axis text { - font-size: 10px; - fill: #333333; - } - - .area.context { - fill: #e4e4e4; - } - - .swimlane .axis text { - display: none; - } - - .swimlane rect.swimlane-cell-hidden { - display: none; - } - - .bottom-swimlane .axis .tick line { - stroke-width: 1px; - } - - .swimlane-cells { - transition: opacity 0.5s; - } - - .swimlane-cell:hover { - opacity: 1; - } - - .brush .extent { - stroke: #fff; - fill-opacity: .125; - shape-rendering: crispEdges; - } - - .progress { - display: inline-block; - min-width: 70px; - margin-bottom: -5px; - margin-left: 49px; - background-color: #FFFFFF; - height: 2px; - border-radius: 0px; - } - - .progress-bar { - text-align: right; - line-height: 18px; - transition: none; - background-color: #32a7c2; - } -} - -// hide the default es loading indicator bar as it can't be switched off -// for standard es searches using the http header. -.loadingIndicator__bar { - display: none; -} +// SASSTODO: This file needs an overwrite. It needs more variables and size calcs +.population-job-container { + font-size: $euiFontSizeS; + width: 100%; + margin-right: auto; + margin-left: auto; + padding-left: $euiSize; + padding-right: $euiSize; + + + .job-state-info { + margin-bottom: $euiSizeS; + } + + .form-controls, .charts-container { + margin: 0px; + margin-right: -$euiSizeS; + + // SASSTODO: Proper selector + & > div { + border: $euiBorderThin; + border-top: 0px; + } + + // SASSTODO: Proper selector + & > h4 { + margin-top: 0px; + margin-bottom: 0px; + padding: $euiSizeS; + background-color: $euiColorPrimary; + color: $euiColorEmptyShade; + } + + .btn-load-vis { + border-radius: $euiBorderRadius !important; + margin-top: -2px; + } + } + + .charts-container { + margin-left: -$euiSizeS; + + label { + margin-bottom: $euiSize; + } + } + + .form-section { + margin: 0px 0px 0px 0px; + border-bottom: $euiBorderThin; + padding: 10px; + overflow: hidden; + + &.form-section-overflow { + overflow: visible; + + .ui-select-bootstrap .ui-select-toggle > a.btn { + margin-top: 0; + border: 0; + } + } + + // SASSTODO: Proper selector + h4, h5 { + margin-top: 0px; + display: inline-block; + } + .form-group:last-child { + margin-bottom: 0px; + } + + .help-text { + border: $euiBorderThin; + padding: $euiSizeXS; + background-color: $euiColorEmptyShade; + border-radius: $euiBorderRadius; + font-size: $euiFontSizeXS; + } + } + + .form-section:last-of-type { + margin: 0px; + border-bottom: 0px solid $euiBorderColor; + } + + // SASSTODO: Proper calc + .form-section-collapsed { + height: 46px; + } + + + .charts-container { + + .chart { + // SASSTODO: Proper selector + label { + display: block; + } + .split-controls { + height: 45px; + + .split-field-container { + width: 255px; + display: inline-block; + .split-field-select { + width: 250px; + display: inline-block; + overflow: hidden; + } + } + .split-align { + vertical-align: top; + margin-top: 7px; + display: inline-block; + } + .remove-split { + height: 22px; + font-size: $euiFontSizeXS; + line-height: $euiFontSizeXS; + margin-top: 6px; + min-width: 100px; + } + } + } + + .card { + // SASSTODO: Proper selector + label { + margin-bottom: $euiSizeS; + } + } + + .split-select { + width: auto; + } + } + + div.validation-error { + color: $euiColorDanger; + font-size: $euiFontSizeXS; + } +} + +.chart-container { + .chart-loader { + border: $euiBorderThin; + margin-left: 49px; + display: flex; + margin-bottom: 25px; + position: relative; + background-color: rgba(255, 255, 255, 0.75); + transition: opacity 0.5s ; + + .status-label { + width: 50%; + text-align: center; + margin: auto; + + .no-results { + color: $euiColorDarkShade; + + // SASSTODO: Proper selector + i { + color: $euiColorPrimary; + } + + // SASSTODO: Proper selector + h2, h4 { + margin-top: 0px; + } + } + } + } + + .event-rate-chart-loader { + margin-left: 0px; + border: none; + } +} + +ml-population-job-chart { + svg { + font-size: $euiFontSizeXS; + font-family: $euiFontFamily; + margin-top: -20px; + } + + .bar { + transition: opacity 0.5s ; + stroke: $euiColorEmptyShade; + } + .bar:hover { + opacity: 1; + } + + .axis path, .axis line { + fill: none; + stroke: $euiBorderColor; + shape-rendering: crispEdges; + } + + .axis text { + fill: $euiTextColor; + } + + .axis .tick line { + stroke: rgba(0, 0, 0, 0.1); + stroke-width: 0px; + } + + .area { + stroke-width: 1; + } + + .area.bounds { + fill: rgba(50, 167, 194, 0.25); + } + + .values-line { + fill: none; + stroke: #32a7c2; + stroke-width: 2; + } + + .values-dots circle { + fill: #32a7c2; + stroke-width: 0; + fill-opacity: 0.5; + } + + .metric-value { + opacity: 1; + fill: #32a7c2; + } + + .context-chart .axis text { + font-size: 10px; + fill: #333333; + } + + .area.context { + fill: #e4e4e4; + } + + .swimlane .axis text { + display: none; + } + + .swimlane rect.swimlane-cell-hidden { + display: none; + } + + .bottom-swimlane .axis .tick line { + stroke-width: 1px; + } + + .swimlane-cells { + transition: opacity 0.5s; + } + + .swimlane-cell:hover { + opacity: 1; + } + + .brush .extent { + stroke: $euiColorEmptyShade; + fill-opacity: .125; + shape-rendering: crispEdges; + } + + .progress { + display: inline-block; + min-width: 70px; + margin-bottom: -5px; + margin-left: 49px; + background-color: $euiColorEmptyShade; + height: 2px; + border-radius: 0px; + } + + .progress-bar { + text-align: right; + line-height: 18px; + transition: none; + background-color: #32a7c2; + } +} + +// hide the default es loading indicator bar as it can't be switched off +// for standard es searches using the http header. +.loadingIndicator__bar { + display: none; +} diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/_index.scss new file mode 100644 index 000000000000000..db63a0bf13dfb40 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/_index.scss @@ -0,0 +1 @@ +@import 'create_job'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job.html b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job.html index 620e16060bda3ac..358260093530998 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job.html +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job.html @@ -230,7 +230,7 @@

No result

-
+
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/index.js index b374fb744635dd3..1ce06a35202b675 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './create_job_controller'; import './create_job_service'; import './create_job_chart_directive'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss new file mode 100644 index 000000000000000..e04ae3b3565598a --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss @@ -0,0 +1 @@ +@import 'create_job/index' \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss old mode 100755 new mode 100644 similarity index 55% rename from x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss index 384638bdf8e184e..100acd586ea37fc --- a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss @@ -1,192 +1,193 @@ -@import (reference) "~ui/styles/variables"; - -.recognizer-job-container { - font-size: 14px; - width: 100%; - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; - - - .job-state-info { - margin-bottom: 10px; - } - - .form-controls, .charts-container { - margin: 0px; - margin-right: -10px; - - & > div { - border: 1px solid #E4E4E3; - border-top: 0px; - } - - & > h4 { - margin-top: 0px; - margin-bottom: 0px; - padding: 10px; - background-color: @kibanaBlue3; - color: #FFFFFF; - } - - .btn-load-vis { - border-radius: 4px !important; - margin-top: -2px; - } - } - - .advanced-button { - min-width: 23px; - } - .advanced-button-container { - label { - cursor: pointer; - display: inline-block; - } - } - .advanced-group { - padding: 10px; - background-color: #F9F9F9; - label { - font-weight: normal; - } - } - - .charts-container { - margin-left: -10px; - margin-bottom: 10px; - margin-right: 0px; - line-height: 20px; - - .jobs-list, .save-objects-list { - padding: 5px; - - .job-container { - border: 1px solid #D9D9D9; - // padding: 5px; - margin: 5px; - border-radius: 3px; - display: flex; - - .labels { - flex: auto; - margin: 5px; - .title { - color: #0079a5; - // font-size: 16px; - } - .exists { - color: #999999; - font-style: italic; - span { - font-size: 12px; - } - } - .sub-title { - color: #999999; - font-size: 12px; - margin-top: 2px; - font-style: italic; - } - } - .results { - flex-grow: 0; - flex-shrink: 0; - // width: 200px; - border-left: 1px solid #D9D9D9; - background-color: #F9F9F9; - // padding: 10px; - padding-top: 4px; - - opacity: 0; - transition: opacity 0.5s; - - .result-box { - display: inline-block; - vertical-align: middle; - text-align: center; - - width: 50px; - - .result-box-title { - color: #999999; - font-size: 12px; - font-style: italic; - margin-bottom: 3px; - } - - .result-box-inner { - // border: 1px solid #D9D9D9; - // background-color: #efefef; - // border-radius: 3px; - // font-size: 20px; - // width: 40px; - // height: 40px; - width: 20px; - height: 20px; - font-size: 15px; - margin: 0px; - align-items: center; - justify-content: center; - display: inline-flex; - vertical-align: top; - } - } - } - } - } - .jobs-list { - .result-box { - margin-right: 10px; - } - } - } - - .form-section { - margin: 0px 0px 0px 0px; - border-bottom: 1px solid #E4E4E3; - padding: 10px; - overflow: hidden; - - & > h4, & > h5 { - margin-top: 0px; - display: inline-block; - } - .form-group:last-child { - margin-bottom: 0px; - } - - .help-text { - border: 1px solid #e8e8e8; - padding: 5px; - background-color: #f1f1f1; - border-radius: 3px; - font-size: 12px; - } - - - } - - .form-section:last-of-type { - margin: 0px; - border-bottom: 0px solid #E4E4E3; - } - - .form-section-collapsed { - height: 46px; - } - - - div.validation-error { - color: #fe5050; - font-size: 12px; - } -} - - -// hide the default es loading indicator bar as it can't be switched off -// for standard es searches using the http header. -.loadingIndicator__bar { - display: none; -} +// SASSTODO: This file needs to be rewritten for proper variable usage and size calcs +.recognizer-job-container { + font-size: $euiFontSizeS; + width: 100%; + margin-right: auto; + margin-left: auto; + padding-left: $euiSize; + padding-right: $euiSize; + + + .job-state-info { + margin-bottom: $euiSize; + } + + .form-controls, .charts-container { + margin: 0px; + margin-right: -$euiSize; + + // SASSTODO: Proper selector + & > div { + border: 1px solid $euiBorderColor; + border-top: 0px; + } + + // SASSTODO: Proper selector + & > h4 { + margin-top: 0px; + margin-bottom: 0px; + padding: 10px; + background-color: $euiColorPrimary; + color: $euiColorEmptyShade; + } + + .btn-load-vis { + border-radius: $euiBorderRadius !important; + margin-top: -2px; + } + } + + // SASSTODO: Proper calc + .advanced-button { + min-width: 23px; + } + + .advanced-button-container { + // SASSTODO: Proper selector + label { + cursor: pointer; + display: inline-block; + } + } + + .advanced-group { + padding: 10px; + background-color: $euiColorLightestShade; + + // SASSTODO: Proper selector + label { + font-weight: $euiFontWeightRegular; + } + } + + .charts-container { + margin-left: -$euiSizeS; + margin-bottom: $euiSizeS; + margin-right: 0px; + line-height: 20px; + + .jobs-list, .save-objects-list { + padding: $euiSizeXS; + + .job-container { + border: 1px solid $euiBorderColor; + // padding: 5px; + margin: $euiSizeXS; + border-radius: $euiBorderRadius; + display: flex; + + .labels { + flex: auto; + margin: $euiBorderRadius; + .title { + color: $euiColorPrimary; + } + .exists { + color: $euiColorMediumShade; + font-style: italic; + + // SASSTODO: Proper selector + span { + font-size: $euiSizeXS; + } + } + .sub-title { + color: $euiColorDarkShade; + font-size: $euiFontSizeXS; + margin-top: 2px; + font-style: italic; + } + } + + .results { + flex-grow: 0; + flex-shrink: 0; + border-left: 1px solid $euiBorderColor; + background-color: $euiColorLightestShade; + padding-top: $euiSizeXS; + + opacity: 0; + transition: opacity 0.5s; + + .result-box { + display: inline-block; + vertical-align: middle; + text-align: center; + // SASSTODO: Proper calc + width: 50px; + + .result-box-title { + color: $euiColorMediumShade; + font-size: $euiFontSizeXS; + font-style: italic; + margin-bottom: $euiSizeXS; + } + + .result-box-inner { + width: 20px; + height: 20px; + font-size: $euiFontSizeM; + margin: 0px; + align-items: center; + justify-content: center; + display: inline-flex; + vertical-align: top; + } + } + } + } + } + .jobs-list { + .result-box { + margin-right: $euiSizeS; + } + } + } + + .form-section { + margin: 0px 0px 0px 0px; + border-bottom: 1px solid $euiBorderColor; + padding: $euiSizeS; + overflow: hidden; + + // SASSTODO: Proper selector + & > h4, & > h5 { + margin-top: 0px; + display: inline-block; + } + .form-group:last-child { + margin-bottom: 0px; + } + + // SASSTODO: Proper selector + .help-text { + border: 1px solid $euiBorderColor; + padding: 5px; + background-color: $euiColorEmptyShade; + border-radius: $euiSizeXS; + font-size: $euiFontSizeXS; + } + } + + .form-section:last-of-type { + margin: 0px; + border-bottom: 0px solid $euiBorderColor; + } + + .form-section-collapsed { + height: 46px; + } + + + div.validation-error { + color: $euiColorDanger; + font-size: $euiFontSizeXS; + } +} + + +// hide the default es loading indicator bar as it can't be switched off +// for standard es searches using the http header. +.loadingIndicator__bar { + display: none; +} diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss new file mode 100644 index 000000000000000..ea8b32f6673b1ec --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss @@ -0,0 +1 @@ +@import 'create_jobs'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html index 84ff772afc856a3..68729812b0690ea 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html @@ -225,7 +225,7 @@

Jobs

{{ui.kibanaLabels[key]}}

-
+
{{obj.title}} diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js index 401240638a69402..cf29ea5a4802ac1 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js @@ -122,7 +122,8 @@ module query, filters: [], useFullIndexData: true, - startDatafeedAfterSave: true + startDatafeedAfterSave: true, + useDedicatedIndex: false, }; $scope.resultsUrl = ''; @@ -250,10 +251,11 @@ module const prefix = $scope.formConfig.jobLabel; const indexPatternName = $scope.formConfig.indexPattern.title; const groups = $scope.formConfig.jobGroups; + const useDedicatedIndex = $scope.formConfig.useDedicatedIndex; const tempQuery = (savedSearch.id === undefined) ? undefined : combinedQuery; - ml.setupDataRecognizerConfig({ moduleId, prefix, groups, query: tempQuery, indexPatternName }) + ml.setupDataRecognizerConfig({ moduleId, prefix, groups, query: tempQuery, indexPatternName, useDedicatedIndex }) .then((resp) => { if (resp.jobs) { $scope.formConfig.jobs.forEach((job) => { diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js index 1c5958eecd323c8..96b3791f4558c91 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './create_job_controller'; import './create_job_service'; import 'plugins/ml/services/mapping_service'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/_index.scss new file mode 100644 index 000000000000000..e75c986b33fbce2 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/_index.scss @@ -0,0 +1 @@ +@import 'create_job/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/_create_job.scss old mode 100755 new mode 100644 similarity index 71% rename from x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/_create_job.scss index f8425eaa0c28892..25fdcee21bb1186 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/_create_job.scss @@ -1,203 +1,210 @@ -.single-metric-job-container { - font-size: 14px; - width: 100%; - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; - - - .job-state-info { - margin-bottom: 10px; - } - - .validation-error { - color: #fe5050; - font-size: 12px; - } - - .form-controls { - .bucket-span-container { - white-space: nowrap; - width: calc(~"100% - 40px"); - margin-bottom: 2px; - - .bucket-span-input { - background-color: transparent; - float: left; - } - - .bucket-span-input:disabled { - background-color: #D9D9D9; - } - - .validation-error { - margin-bottom: -25px; - } - } - - .btn-load-vis { - border-radius: 4px !important; - margin-left: 5px; - min-width: 35px; - } - } - - .advanced-button { - min-width: 23px; - } - .advanced-button-container { - label { - cursor: pointer; - display: inline-block; - } - } - - .advanced-group { - padding: 10px; - background-color: #F9F9F9; - label { - font-weight: normal; - } - } - - .chart-container { - margin-bottom: 20px; - - .chart-loader { - border: 1px solid #e5e5e5; - margin-left: 49px; - display: flex; - margin-bottom: 52px; - position: relative; - background-color: rgba(255, 255, 255, 0.75); - - .status-label { - width: 50%; - text-align: center; - margin: auto; - - - .no-results { - color: #555; - background-color: aliceblue; - border: 1px solid #DDEFFF; - padding: 25px; - border-radius: 4px; - display: inline-block; - - i { - color: #5a9bd6 - } - h2, h4 { - margin-top: 0px; - } - } - } - } - - ml-single-metric-job-chart { - svg { - font-size: 12px; - font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial; - } - - .axis path, .axis line { - fill: none; - stroke: #cccccc; - shape-rendering: crispEdges; - } - - .axis text { - fill: #000; - } - - .axis .tick line { - stroke: rgba(0, 0, 0, 0.1); - stroke-width: 0px; - } - - .area { - stroke-width: 1; - } - - .area.bounds { - fill: rgba(50, 167, 194, 0.25); - } - - .values-line { - fill: none; - stroke: #32a7c2; - stroke-width: 2; - } - - .values-dots circle { - fill: #32a7c2; - stroke-width: 0; - } - - .metric-value { - opacity: 1; - fill: #32a7c2; - } - - .context-chart .axis text { - font-size: 10px; - fill: #333333; - } - - .area.context { - fill: #e4e4e4; - } - - .swimlane .axis text { - display: none; - } - - .swimlane rect.swimlane-cell-hidden { - display: none; - } - - .model-chart, .swimlane { - transition: opacity 0.5s ; - } - - .bottom-swimlane .axis .tick line { - stroke-width: 1px; - } - - .swimlane-cell:hover { - opacity: 1; - } - - .brush .extent { - stroke: #fff; - fill-opacity: .125; - shape-rendering: crispEdges; - } - - .progress { - display: inline-block; - min-width: 70px; - margin-bottom: -5px; - margin-left: 49px; - background-color: #FFFFFF; - height: 2px; - border-radius: 0px; - } - - .progress-bar { - text-align: right; - line-height: 18px; - transition: none; - background-color: #32a7c2; - } - } - } -} - -// hide the default es loading indicator bar as it can't be switched off -// for standard es searches using the http header. -.loadingIndicator__bar { - display: none; -} +// SASSTODO: Thie file needs a rewrite to add proper variables and sizing +.single-metric-job-container { + font-size: $euiFontSizeS; + width: 100%; + margin-right: auto; + margin-left: auto; + padding-left: $euiSize; + padding-right: $euiSize; + + + .job-state-info { + margin-bottom: $euiSizeS; + } + + .validation-error { + color: $euiColorDanger; + font-size: 12px; + } + + .form-controls { + .bucket-span-container { + white-space: nowrap; + width: calc(100% - 40px); + margin-bottom: 2px; + + .bucket-span-input { + background-color: transparent; + float: left; + } + + .bucket-span-input:disabled { + background-color: $euiColorLightShade; + } + + .validation-error { + margin-bottom: -$euiSizeL; + } + } + + .btn-load-vis { + border-radius: $euiBorderRadius !important; + margin-left: $euiSizeXS; + min-width: 35px; // SASSTODO: Proper calc + } + } + + // SASSTODO: Proper calc + .advanced-button { + min-width: 23px; + } + + .advanced-button-container { + // SASSTODO: Proper selector + label { + cursor: pointer; + display: inline-block; + } + } + + .advanced-group { + padding: $euiSizeS; + background-color: $euiColorLightestShade; + label { + font-weight: normal; + } + } + + .chart-container { + margin-bottom: 20px; + + .chart-loader { + border: 1px solid $euiBorderColor; + margin-left: 49px; + display: flex; + margin-bottom: 52px; + position: relative; + background-color: rgba(255, 255, 255, 0.75); + + .status-label { + width: 50%; + text-align: center; + margin: auto; + + + .no-results { + color: $euiColorDarkShade; + background-color: $euiColorLightestShade; + border: $euiBorderColor; + padding: $euiSizeL; + border-radius: $euiSizeXS; + display: inline-block; + + // SASSTODO: Proper selector + i { + color: $euiColorPrimary; + } + + // SASSTODO: Proper selector + h2, h4 { + margin-top: 0px; + } + } + } + } + + ml-single-metric-job-chart { + svg { + font-size: $euiSizeXS; + font-family: $euiFontFamily; + } + + .axis path, .axis line { + fill: none; + stroke: $euiBorderColor; + shape-rendering: crispEdges; + } + + .axis text { + fill: $euiTextColor; + } + + .axis .tick line { + stroke: rgba(0, 0, 0, 0.1); + stroke-width: 0px; + } + + .area { + stroke-width: 1; + } + + .area.bounds { + fill: rgba(50, 167, 194, 0.25); + } + + .values-line { + fill: none; + stroke: #32a7c2; + stroke-width: 2; + } + + .values-dots circle { + fill: #32a7c2; + stroke-width: 0; + } + + .metric-value { + opacity: 1; + fill: #32a7c2; + } + + .context-chart .axis text { + font-size: 10px; + fill: #333333; + } + + .area.context { + fill: #e4e4e4; + } + + .swimlane .axis text { + display: none; + } + + .swimlane rect.swimlane-cell-hidden { + display: none; + } + + .model-chart, .swimlane { + transition: opacity 0.5s ; + } + + .bottom-swimlane .axis .tick line { + stroke-width: 1px; + } + + .swimlane-cell:hover { + opacity: 1; + } + + .brush .extent { + stroke: $euiColorEmptyShade; + fill-opacity: .125; + shape-rendering: crispEdges; + } + + .progress { + display: inline-block; + min-width: 70px; + margin-bottom: -5px; + margin-left: 49px; + background-color: $euiColorEmptyShade; + height: 2px; + border-radius: 0px; + } + + .progress-bar { + text-align: right; + line-height: 18px; + transition: none; + background-color: #32a7c2; + } + } + } +} + +// hide the default es loading indicator bar as it can't be switched off +// for standard es searches using the http header. +.loadingIndicator__bar { + display: none; +} diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/_index.scss new file mode 100644 index 000000000000000..db63a0bf13dfb40 --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/_index.scss @@ -0,0 +1 @@ +@import 'create_job'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/index.js b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/index.js index 5d97c601cae5ff1..d2d922840b98ee9 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './create_job_controller'; import './create_job_service'; import './create_job_chart_directive'; diff --git a/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js b/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js index cf55e0d43e1a8f3..f4b5e2a2c5597a0 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js +++ b/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js @@ -115,3 +115,44 @@ export function focusOnResultsLink(linkId, $timeout) { $(`#${linkId}`).focus(); }, 0); } + +// Only model plot cardinality relevant +// format:[{id:"cardinality_model_plot_high",modelPlotCardinality:11405}, {id:"cardinality_partition_field",fieldName:"clientip"}] +export function checkCardinalitySuccess(data) { + const response = { + success: true, + }; + // There were no fields to run cardinality on. + if (Array.isArray(data) && data.length === 0) { + return response; + } + + for (let i = 0; i < data.length; i++) { + if (data[i].id === 'success_cardinality') { + break; + } + + if (data[i].id === 'cardinality_model_plot_high') { + response.success = false; + response.highCardinality = data[i].modelPlotCardinality; + break; + } + } + + return response; +} + +// Ensure validation endpoints are given job with expected minimum fields +export function getMinimalValidJob() { + return { + analysis_config: { + bucket_span: '15m', + detectors: [], + influencers: [] + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [] + } + }; +} diff --git a/x-pack/plugins/ml/public/jobs/new_job/wizard/_index.scss b/x-pack/plugins/ml/public/jobs/new_job/wizard/_index.scss new file mode 100644 index 000000000000000..ae17bd2da34797c --- /dev/null +++ b/x-pack/plugins/ml/public/jobs/new_job/wizard/_index.scss @@ -0,0 +1 @@ +@import 'wizard'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/jobs/new_job/wizard/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/wizard/_wizard.scss similarity index 53% rename from x-pack/plugins/ml/public/jobs/new_job/wizard/styles/main.less rename to x-pack/plugins/ml/public/jobs/new_job/wizard/_wizard.scss index 61fa2adbb6c4859..bea387d7819830d 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/wizard/styles/main.less +++ b/x-pack/plugins/ml/public/jobs/new_job/wizard/_wizard.scss @@ -1,40 +1,28 @@ -@import (reference) '~ui/styles/variables/colors'; -@import (reference) "~ui/styles/variables"; - .job-type-gallery { width: 100%; - padding-right: 10px; - padding-left: 10px; - background-color: @globalColorLightestGray; - -webkit-box-flex: 1; - -webkit-flex: 1 0 auto; - -ms-flex: 1 0 auto; + padding-right: $euiSizeS; + padding-left: $euiSizeS; + background-color: $euiColorLightestShade; flex: 1 0 auto; .job-types-content { - max-width: 1200px; + max-width: 1200px; // SASSTODO: Proper calc margin-right: auto; margin-left: auto; } .synopsis { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; flex-grow: 1; .synopsisTitle { - font-size: 16px; + font-size: $euiFontSize; font-weight: normal; - color: #0079a5; + color: $euiColorPrimary; } .synopsisIcon { - padding-top: 8px; + padding-top: $euiSizeS; } } @@ -54,12 +42,12 @@ pointer-events: none; .synopsisTitle { - color: grey; + color: $euiColorDarkShade; } } .index-warning { - border: 1px solid #d9d9d9; + border: $euiBorderThin; } } diff --git a/x-pack/plugins/ml/public/jobs/new_job/wizard/index.js b/x-pack/plugins/ml/public/jobs/new_job/wizard/index.js index 2fc3cb33d677254..c2fb46463066929 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/wizard/index.js +++ b/x-pack/plugins/ml/public/jobs/new_job/wizard/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; // SASS TODO: Import wizard.scss instead // import 'plugins/kibana/visualize/wizard/wizard.less'; import './steps/index_or_search'; diff --git a/x-pack/plugins/ml/public/jobs/styles/main.less b/x-pack/plugins/ml/public/jobs/styles/main.less deleted file mode 100644 index 506d8b7087a37de..000000000000000 --- a/x-pack/plugins/ml/public/jobs/styles/main.less +++ /dev/null @@ -1,6 +0,0 @@ -@import (reference) "~ui/styles/theme"; -@import (reference) "~ui/styles/variables"; - -.modal { - cursor: default; -} diff --git a/x-pack/plugins/ml/public/services/field_format_service.js b/x-pack/plugins/ml/public/services/field_format_service.js index c02160c15a9fd7b..60d3cdeeb901db8 100644 --- a/x-pack/plugins/ml/public/services/field_format_service.js +++ b/x-pack/plugins/ml/public/services/field_format_service.js @@ -8,9 +8,9 @@ import _ from 'lodash'; -import { mlFunctionToESAggregation } from 'plugins/ml/../common/util/job_utils'; -import { getIndexPatternById } from 'plugins/ml/util/index_utils'; -import { mlJobService } from 'plugins/ml/services/job_service'; +import { mlFunctionToESAggregation } from '../../common/util/job_utils'; +import { getIndexPatternById } from '../util/index_utils'; +import { mlJobService } from '../services/job_service'; // Service for accessing FieldFormat objects configured for a Kibana index pattern // for use in formatting the actual and typical values from anomalies. @@ -121,4 +121,3 @@ class FieldFormatService { } export const mlFieldFormatService = new FieldFormatService(); - diff --git a/x-pack/plugins/ml/public/services/ml_api_service/index.js b/x-pack/plugins/ml/public/services/ml_api_service/index.js index b43b3a0b31c3cac..43100238f9d7e92 100644 --- a/x-pack/plugins/ml/public/services/ml_api_service/index.js +++ b/x-pack/plugins/ml/public/services/ml_api_service/index.js @@ -258,7 +258,8 @@ export const ml = { 'prefix', 'groups', 'indexPatternName', - 'query' + 'query', + 'useDedicatedIndex' ]); return http({ diff --git a/x-pack/plugins/ml/public/settings/_index.scss b/x-pack/plugins/ml/public/settings/_index.scss new file mode 100644 index 000000000000000..fa32ea3cbff3421 --- /dev/null +++ b/x-pack/plugins/ml/public/settings/_index.scss @@ -0,0 +1,3 @@ +@import 'settings'; +@import 'filter_lists/index'; +@import 'scheduled_events/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/_settings.scss b/x-pack/plugins/ml/public/settings/_settings.scss new file mode 100644 index 000000000000000..281e1156b79e1af --- /dev/null +++ b/x-pack/plugins/ml/public/settings/_settings.scss @@ -0,0 +1,18 @@ +.disabled { + color: $euiColorMediumShade; + cursor: pointer; +} + +ml-settings { + .mgtPanel { + .mgtPanel__link { + font-size: $euiFontSizeM; + line-height: $euiSizeXL; + margin-left: $euiSizeXS; + } + + .disabled { + color: $euiColorMediumShade; + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/filter_lists/list/styles/main.less b/x-pack/plugins/ml/public/settings/filter_lists/_filter_lists.scss similarity index 57% rename from x-pack/plugins/ml/public/settings/filter_lists/list/styles/main.less rename to x-pack/plugins/ml/public/settings/filter_lists/_filter_lists.scss index 961b748d54f119f..80dd1c495b092b8 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/list/styles/main.less +++ b/x-pack/plugins/ml/public/settings/filter_lists/_filter_lists.scss @@ -1,9 +1,14 @@ +.ml-filter-lists { + background: $euiColorLightestShade; + min-height: 100vh; +} + .ml-list-filter-lists { .ml-list-filter-lists-content { max-width: 1100px; - margin-top: 16px; - margin-bottom: 16px; + margin-top: $euiSize; + margin-bottom: $euiSize; } .ml-filter-lists-table { diff --git a/x-pack/plugins/ml/public/settings/filter_lists/_index.scss b/x-pack/plugins/ml/public/settings/filter_lists/_index.scss new file mode 100644 index 000000000000000..049d1f4cde5d589 --- /dev/null +++ b/x-pack/plugins/ml/public/settings/filter_lists/_index.scss @@ -0,0 +1,2 @@ +@import 'filter_lists'; +@import 'edit/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/filter_lists/edit/styles/main.less b/x-pack/plugins/ml/public/settings/filter_lists/edit/_edit.scss similarity index 69% rename from x-pack/plugins/ml/public/settings/filter_lists/edit/styles/main.less rename to x-pack/plugins/ml/public/settings/filter_lists/edit/_edit.scss index 1fc0c0e190b1369..55978f1fb704a13 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/edit/styles/main.less +++ b/x-pack/plugins/ml/public/settings/filter_lists/edit/_edit.scss @@ -2,8 +2,8 @@ .ml-edit-filter-lists-content { max-width: 1100px; width: 100%; - margin-top: 16px; - margin-bottom: 16px; + margin-top: $euiSize; + margin-bottom: $euiSize; } .ml-filter-list-usage > div { @@ -12,11 +12,11 @@ .ml-filter-list-usage { .euiButtonEmpty.euiButtonEmpty--small { - padding-bottom: 3px; + padding-bottom: $euiSizeXS; } .euiButtonEmpty .euiButtonEmpty__content { - padding: 0px 4px; + padding: 0px $euiSizeXS; } } } @@ -24,12 +24,12 @@ .ml-add-filter-item-popover { .euiFormRow { width: 300px; - padding-bottom: 4px; + padding-bottom: $euiSizeXS; } } .ml-filter-list-usage-popover { li { - line-height: 24px; + line-height: $euiSizeL; } } diff --git a/x-pack/plugins/ml/public/settings/filter_lists/edit/_index.scss b/x-pack/plugins/ml/public/settings/filter_lists/edit/_index.scss new file mode 100644 index 000000000000000..4272742ff49b1fd --- /dev/null +++ b/x-pack/plugins/ml/public/settings/filter_lists/edit/_index.scss @@ -0,0 +1 @@ +@import 'edit'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/filter_lists/edit/index.js b/x-pack/plugins/ml/public/settings/filter_lists/edit/index.js index bdbb6120391f387..5913b7ef60495cc 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/edit/index.js +++ b/x-pack/plugins/ml/public/settings/filter_lists/edit/index.js @@ -5,5 +5,4 @@ */ -import './directive'; -import './styles/main.less'; +import './directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/filter_lists/index.js b/x-pack/plugins/ml/public/settings/filter_lists/index.js index 5fb5e06aa5aa91b..3e610e6e8ddf006 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/index.js +++ b/x-pack/plugins/ml/public/settings/filter_lists/index.js @@ -6,5 +6,4 @@ import './edit'; -import './list'; -import './styles/main.less'; +import './list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/filter_lists/list/index.js b/x-pack/plugins/ml/public/settings/filter_lists/list/index.js index 4bbbfcae5dc205d..fd75a9ceb9b49cf 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/list/index.js +++ b/x-pack/plugins/ml/public/settings/filter_lists/list/index.js @@ -7,4 +7,3 @@ import './directive'; -import './styles/main.less'; diff --git a/x-pack/plugins/ml/public/settings/filter_lists/styles/main.less b/x-pack/plugins/ml/public/settings/filter_lists/styles/main.less deleted file mode 100644 index b4a49181529874d..000000000000000 --- a/x-pack/plugins/ml/public/settings/filter_lists/styles/main.less +++ /dev/null @@ -1,4 +0,0 @@ -.ml-filter-lists { - background: #F5F5F5; - min-height: 100vh; -} diff --git a/x-pack/plugins/ml/public/settings/index.js b/x-pack/plugins/ml/public/settings/index.js index dda643fce713bcc..414587ae8d9ce29 100644 --- a/x-pack/plugins/ml/public/settings/index.js +++ b/x-pack/plugins/ml/public/settings/index.js @@ -6,7 +6,6 @@ -import './styles/main.less'; import './settings_controller'; import './scheduled_events'; import './filter_lists'; diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/_index.scss b/x-pack/plugins/ml/public/settings/scheduled_events/_index.scss new file mode 100644 index 000000000000000..a5db98c4b12295e --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/_index.scss @@ -0,0 +1,6 @@ +@import 'calendars_list/index'; +@import 'new_calendar/index'; + +@import 'components/events_list/index'; +@import 'components/import_events_modal/index'; +@import 'components/new_event_modal/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/styles/main.less b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/_calendars_list.scss similarity index 58% rename from x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/styles/main.less rename to x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/_calendars_list.scss index 1b94cdf1367f13a..963c9d0e2cd54e0 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/styles/main.less +++ b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/_calendars_list.scss @@ -1,17 +1,19 @@ ml-calendars-list{ - font-size: 14px; + font-size: $euiFontSizeS; + header { - margin: 15px 0; + margin: $euiSize 0; } .calendars-list-container { width: 100%; margin-right: auto; margin-left: auto; - padding-left: 15px; - padding-right: 15px; + padding-left: $euiSize; + padding-right: $euiSize; } .actions-col { + // SASSTODO: Proper calcs width: 150px; min-width: 150px; } diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/_index.scss b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/_index.scss new file mode 100644 index 000000000000000..98af83f63380638 --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/_index.scss @@ -0,0 +1 @@ +@import 'calendars_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/index.js b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/index.js index 63a9810203dce77..6a6382516e06fda 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/index.js +++ b/x-pack/plugins/ml/public/settings/scheduled_events/calendars_list/index.js @@ -6,5 +6,4 @@ -import './styles/main.less'; import './calendars_list_controller'; diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/_events_list.scss b/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/_events_list.scss new file mode 100644 index 000000000000000..b5d4b9056f1ecbe --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/_events_list.scss @@ -0,0 +1,19 @@ +.events-list { + .actions-col { + width: 90px; + min-width: 90px; + } + .asterisk { + color: $euiColorDanger; + font-weight: $euiFontWeightBold; + cursor: default; + } + + .footer-cell { + border-top: $euiBorderThin; + padding: $euiSizeS; + font-size: $euiFontSizeS; + font-style: italic; + text-align: right; + } +} diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/_index.scss b/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/_index.scss new file mode 100644 index 000000000000000..d5d95b9d63ccce3 --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/_index.scss @@ -0,0 +1 @@ +@import 'events_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/index.js b/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/index.js index 135f08ceadd3055..f6bfb42f65aed36 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/index.js +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/index.js @@ -6,5 +6,4 @@ -import './styles/main.less'; import './events_list_directive'; diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/styles/main.less b/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/styles/main.less deleted file mode 100644 index e3e4cef98ada579..000000000000000 --- a/x-pack/plugins/ml/public/settings/scheduled_events/components/events_list/styles/main.less +++ /dev/null @@ -1,21 +0,0 @@ -@import (reference) "~ui/styles/variables"; - -.events-list { - .actions-col { - width: 90px; - min-width: 90px; - } - .asterisk { - color: @kibanaRed1; - font-weight: bold; - cursor: default; - } - - .footer-cell { - border-top: 1px solid @globalColorLightGray; - padding: 7px 8px 8px; - font-size: 12px; - font-style: italic; - text-align: right; - } -} diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/styles/main.less b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/_import_events_modal.scss similarity index 51% rename from x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/styles/main.less rename to x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/_import_events_modal.scss index 14cfe5e64c05787..c827578661e60a3 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/styles/main.less +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/_import_events_modal.scss @@ -1,8 +1,6 @@ -@import (reference) "~ui/styles/variables"; - .import-events-modal { - font-size: 14px; - padding:20px; + font-size: $euiFontSizeS; + padding: $euiSizeL; cursor: auto; .no-shadow { @@ -10,6 +8,6 @@ } .recurring-warning { - color: @kibanaRed1; + color: $euiColorDanger; } } diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/_index.scss b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/_index.scss new file mode 100644 index 000000000000000..b7a72ef464ddacc --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/_index.scss @@ -0,0 +1 @@ +@import 'import_events_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/index.js b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/index.js index 20e86d1124b909d..e0cb4cf7a574b55 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/index.js +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/import_events_modal/index.js @@ -6,6 +6,5 @@ -import './styles/main.less'; import './import_events_service'; import './import_events_modal_controller'; diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/_index.scss b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/_index.scss new file mode 100644 index 000000000000000..a9f6e72195d7228 --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/_index.scss @@ -0,0 +1 @@ +@import 'new_event_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/styles/main.less b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/_new_event_modal.scss similarity index 67% rename from x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/styles/main.less rename to x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/_new_event_modal.scss index 344f0a33292cec1..71f770c3e89dfd3 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/styles/main.less +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/_new_event_modal.scss @@ -1,6 +1,6 @@ .new-event-modal { - font-size: 14px; - padding:20px; + font-size: $euiFontSizeS; + padding: $euiSizeL; cursor: auto; .date_container { @@ -9,16 +9,17 @@ } .ml-new-event-contents { - margin-top: 5px; + margin-top: $euiSizeXS; .main-container { display: flex; .tabs-container { + // SASSTODO: Proper selectors ul { padding: 0px; li { a { - border-radius: 4px 0px 0px 4px; + border-radius: $euiSizeXS 0px 0px $euiSizeXS; white-space: nowrap; } } @@ -26,10 +27,11 @@ } .calendar-container { - border: 2px solid #0079a5; + border: 2px solid $euiColorPrimary; flex-grow: 1; - border-radius: 4px; + border-radius: $euiBorderRadius; + // SASSTODO: Proper calcs .calendar { width: 235px; margin: 20px; @@ -38,7 +40,7 @@ margin-right: -33px; } .partition { - border-right: 1px solid #0079a5; + border-right: 1px solid $euiColorPrimary; position: absolute; height: 275px; left: 270px; @@ -50,7 +52,8 @@ } } .top-tab-selected { - border-radius: 0px 4px 4px 4px; + border-radius: 0 $euiSizeXS $euiSizeXS $euiSizeXS; + } } } diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/index.js b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/index.js index de6ebcc9a33a3ff..9bda4b44c11897b 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/index.js +++ b/x-pack/plugins/ml/public/settings/scheduled_events/components/new_event_modal/index.js @@ -6,6 +6,5 @@ -import './styles/main.less'; import './new_event_service'; import './new_event_modal_controller'; diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/_index.scss b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/_index.scss new file mode 100644 index 000000000000000..dc8fbb6efc0f57d --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/_index.scss @@ -0,0 +1 @@ +@import 'new_calendar'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/_new_calendar.scss b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/_new_calendar.scss new file mode 100644 index 000000000000000..fc393ce56ce757a --- /dev/null +++ b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/_new_calendar.scss @@ -0,0 +1,21 @@ +ml-new-calendar{ + font-size: $euiFontSizeS; + + // SASSTODO: Proper selector + h3 { + margin-top: 0px; + } + + .new-calendar-container { + width: 100%; + margin-right: auto; + margin-left: auto; + padding-left: $euiSize; + padding-right: $euiSize; + + .validation-error { + color: $euiColorDanger; + font-size: $euiFontSizeXS; + } + } +} diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/index.js b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/index.js index 66c2ccd0e560e03..6dd769aa0aa23c7 100644 --- a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/index.js +++ b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/index.js @@ -6,5 +6,4 @@ -import './styles/main.less'; import './create_calendar_controller'; diff --git a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/styles/main.less b/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/styles/main.less deleted file mode 100644 index e080af6af7642f7..000000000000000 --- a/x-pack/plugins/ml/public/settings/scheduled_events/new_calendar/styles/main.less +++ /dev/null @@ -1,18 +0,0 @@ -ml-new-calendar{ - font-size: 14px; - h3 { - margin-top: 0px; - } - .new-calendar-container { - width: 100%; - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; - - .validation-error { - color: #fe5050; - font-size: 12px; - } - } -} diff --git a/x-pack/plugins/ml/public/settings/styles/main.less b/x-pack/plugins/ml/public/settings/styles/main.less deleted file mode 100644 index 2af8fb0ebdb8a6a..000000000000000 --- a/x-pack/plugins/ml/public/settings/styles/main.less +++ /dev/null @@ -1,22 +0,0 @@ -@import (reference) "~ui/styles/theme"; -@import (reference) "~ui/styles/variables"; - -.disabled { - color: #999; - cursor: pointer; -} - -ml-settings { - .mgtPanel { - .mgtPanel__link { - font-size: 17px; - line-height: 32px; - margin-left: 6px; - } - .disabled { - color: silver; - } - } -} - - diff --git a/x-pack/plugins/ml/public/styles/icons.less b/x-pack/plugins/ml/public/styles/icons.less deleted file mode 100644 index b7fdf41ecebef0b..000000000000000 --- a/x-pack/plugins/ml/public/styles/icons.less +++ /dev/null @@ -1,27 +0,0 @@ -.ml-icon-severity-critical, -.ml-icon-severity-major, -.ml-icon-severity-minor, -.ml-icon-severity-warning, -.ml-icon-severity-unknown { - text-shadow: 1px 1px 1px #BBBBBB; -} - -.ml-icon-severity-critical { - color: #fe5050; -} - -.ml-icon-severity-major { - color: #fba740; -} - -.ml-icon-severity-minor { - color: #fdec25; -} - -.ml-icon-severity-warning { - color: #8bc8fb; -} - -.ml-icon-severity-unknown { - color: #c0c0c0; -} diff --git a/x-pack/plugins/ml/public/styles/main.less b/x-pack/plugins/ml/public/styles/main.less deleted file mode 100644 index 4a84463f92dd7f0..000000000000000 --- a/x-pack/plugins/ml/public/styles/main.less +++ /dev/null @@ -1,27 +0,0 @@ -@import "./icons"; -@import "./ui-select"; - -// Hacks. These need to be removed when ML converts to EUI. -.tab-jobs, -.edit-job-modal, -.create-watch-modal { - label { - display: inline-block; - } - - .validation-error { - margin-top: 5px; - } -} - -.button-wrapper { - display: inline; -} - -.button-wrapper.disabled .kuiButton[disabled] { - pointer-events: none; -} - -.button-wrapper.disabled { - cursor: not-allowed; -} diff --git a/x-pack/plugins/ml/public/styles/ui-select.less b/x-pack/plugins/ml/public/styles/ui-select.less deleted file mode 100644 index 51d2cfd046d9a2c..000000000000000 --- a/x-pack/plugins/ml/public/styles/ui-select.less +++ /dev/null @@ -1,30 +0,0 @@ -@import (reference) "~ui/styles/variables"; - -// overriding the style of the ui-select element when disabled. To make it look the same all other standard selects - -.ui-select-match { - .btn-default[disabled], - .btn-default[disabled]:hover, - .btn-default[disabled]:focus { - background-color: @input-bg-disabled; - border-color: @input-bg-disabled; - opacity: 1; - - .ui-select-placeholder { - color: @kibanaGray1; - } - } - } - - .ui-select-container input[type="search"] { - padding-left: 15px; - padding-top: 4px; - } - -.ui-select-container input[type="search"]::placeholder { - color: @input-color-placeholder; -} - -.ui-select-container input[type="search"]:focus { - box-shadow: none; -} diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/_index.scss b/x-pack/plugins/ml/public/timeseriesexplorer/_index.scss new file mode 100644 index 000000000000000..22707a81d66fe49 --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/_index.scss @@ -0,0 +1,2 @@ +@import 'timeseriesexplorer'; +@import 'forecasting_modal/index'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/styles/main.less b/x-pack/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss similarity index 66% rename from x-pack/plugins/ml/public/timeseriesexplorer/styles/main.less rename to x-pack/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss index 1900000c6bd4b55..8e2d89f447be041 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/styles/main.less +++ b/x-pack/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss @@ -1,54 +1,53 @@ .ml-time-series-explorer { width: 100%; display: inline-block; - color: #555; + color: $euiColorDarkShade; .no-results-container { text-align: center; - font-size: 17px; - padding-top: 60px; + font-size: $euiFontSizeL; + padding-top: 60px; // SASSTODO: variabalize .no-results { - background-color: aliceblue; - border: 1px solid #DDEFFF; - padding: 15px; - border-radius: 4px; + background-color: $euiFocusBackgroundColor; + padding: $euiSize; + border-radius: $euiBorderRadius; display: inline-block; i { - color: #5a9bd6; - margin-right: 5px; + color: $euiColorPrimary; + margin-right: $euiSizeXS; } div:nth-child(2) { - margin-top: 10px; - font-size: 13px; + margin-top: $euiSizeS; + font-size: $euiFontSizeXS; } } } .results-container { - padding: 15px; + padding: $euiSize; .panel-title { - color: #7b8a8b; + color: $euiTitleColor; } .entity-count-text { - color: #7b8a8b; - font-size: 13px; + color: $euiColorSecondary; + font-size: $euiFontSizeS; } } .series-controls { - padding: 15px 15px 0px 15px; + padding: $euiSize $euiSize 0px $euiSize; div.entity-controls { display: inline-block; - padding-left: 10px; + padding-left: $euiSize; input.entity-input-blank { - border-color: red; + border-color: $euiColorDanger; } .entity-input { @@ -57,7 +56,7 @@ } button { - margin-left: 5px; + margin-left: $euiSizeXS; } } @@ -68,12 +67,12 @@ div { display: inline; - padding-left: 15px; + padding-left: $euiSize; } .kuiCheckBoxLabel { display: inline-block; - font-size: 12px; + font-size: $euiFontSizeXS; } } @@ -82,23 +81,23 @@ } .ml-anomalies-controls { - padding-top: 5px; + padding-top: $euiSizeXS; } .ml-timeseries-chart { svg { - font-size: 12px; - font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial; + font-size: $euiFontSizeXS; + font-family: $euiFontFamily; } .axis path, .axis line { fill: none; - stroke: #cccccc; + stroke: $euiBorderColor; shape-rendering: crispEdges; } .axis text { - fill: #000; + fill: $euiTextColor; } .axis .tick line { @@ -106,14 +105,14 @@ } .chart-border { - stroke: #cccccc; + stroke: $euiBorderColor; fill: none; stroke-width: 1; shape-rendering: crispEdges; } .chart-border-highlight { - stroke: #6A6A6A; + stroke: $euiColorDarkShade; stroke-width: 2; } .chart-border-highlight:hover { @@ -125,21 +124,21 @@ } .area.bounds { - fill: rgba(50, 167, 194, 0.25); + fill: rgba(50, 167, 194, 0.25); // Needs variable } .values-line { fill: none; - stroke: #32a7c2; + stroke: #32a7c2; // Needs variable stroke-width: 2; } .values-line.forecast { - stroke: #cca300; + stroke: #cca300; // Needs variable } .values-dots circle { - fill: #32a7c2; + fill: #32a7c2; // Needs variable stroke-width: 0; } @@ -148,7 +147,7 @@ } .area.forecast { - fill: rgba(204, 163, 0, 0.25); + fill: rgba(204, 163, 0, 0.25); // Needs variable } .metric-value { @@ -160,27 +159,27 @@ .anomaly-marker { stroke-width: 1px; - stroke: #aaaaaa; + stroke: #aaaaaa; // Needs variable } .anomaly-marker.critical { - fill: #fe5050; + fill: $mlColorCritical; } .anomaly-marker.major { - fill: #ff7e00; + fill: $mlColorMajor; } .anomaly-marker.minor { - fill: #ffdd00; + fill: $mlColorMinor; } .anomaly-marker.warning { - fill: #8bc8fb; + fill: $mlColorWarning; } .anomaly-marker.low { - fill: #d2e9f7; + fill: #d2e9f7; // Needs variable } .metric-value:hover, @@ -193,8 +192,8 @@ rect.scheduled-event-marker { stroke-width: 1px; - stroke: #999999; - fill: #cccccc; + stroke: $euiColorMediumShade; + fill: $euiColorLightShade; pointer-events: none; } @@ -210,20 +209,20 @@ line { fill: none; shape-rendering: crispEdges; - stroke: #cccccc; + stroke: $euiColorLightestShade; } rect { - fill: #f1f1f1; + fill: $euiColorLightestShade; } } .focus-zoom { - fill: #555555; + fill: $euiColorDarkShade; a { text { - fill: #328caa; + fill: $euiColorPrimary; cursor: pointer; } } @@ -242,11 +241,11 @@ .axis text { font-size: 10px; - fill: #333333; + fill: $euiTextColor; } .area.context { - fill: rgba(50, 167, 194, 0.25); + fill: rgba(50, 167, 194, 0.25); // Needs variable } .values-line { @@ -254,7 +253,7 @@ } .area.context.forecast { - fill: rgba(204, 163, 0, 0.25); + fill: rgba(204, 163, 0, 0.25); // Needs variable } .mask { @@ -294,7 +293,7 @@ } .brush .extent { - stroke: #6A6A6A; + stroke: $euiColorDarkShade; stroke-width: 2; cursor: move; } @@ -304,7 +303,7 @@ } .top-border { - fill: #FFFFFF; + fill: $euiColorEmptyShade; } foreignObject.brush-handle { @@ -313,8 +312,8 @@ } div.brush-handle-inner { - border: 1px solid #6A6A6A; - background-color: #CCCCCC; + border: 1px solid $euiColorDarkShade; + background-color: $euiColorLightShade; height: 70px; width: 10px; text-align: center; @@ -327,16 +326,16 @@ } div.brush-handle-inner-left { - border-radius: 4px 0px 0px 4px; + border-radius: $euiBorderRadius 0px 0px $euiBorderRadius; } div.brush-handle-inner-right { - border-radius: 0px 4px 4px 0px; + border-radius: 0px $euiBorderRadius $euiBorderRadius 0px; } rect.brush-handle { stroke-width: 1; - stroke: #6A6A6A; - fill: #CCCCCC; + stroke: $euiColorDarkShade; + fill: $euiColorLightShade; pointer-events: none; } diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/context_chart_mask.js b/x-pack/plugins/ml/public/timeseriesexplorer/context_chart_mask.js index 8f0cd6cd4947e5f..3925f2d59a9462b 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/context_chart_mask.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/context_chart_mask.js @@ -8,7 +8,7 @@ import d3 from 'd3'; -import { drawLineChartDots } from 'plugins/ml/util/chart_utils'; +import { drawLineChartDots } from '../util/chart_utils'; /* * Creates a mask over sections of the context chart and swimlane diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/styles/main.less b/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/_forecasting_modal.scss similarity index 80% rename from x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/styles/main.less rename to x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/_forecasting_modal.scss index 549d2fff654ca77..fe969a5800c5787 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/styles/main.less +++ b/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/_forecasting_modal.scss @@ -6,13 +6,14 @@ width: 60px; } + // SASSTODO: Proper calcs .view-forecast-btn { width: 35px; min-width: 35px; height: 24px; .euiButton__content { - padding: 0px 8px; + padding: 0px $euiSizeS; } } } diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/_index.scss b/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/_index.scss new file mode 100644 index 000000000000000..67985187665bd5e --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/_index.scss @@ -0,0 +1 @@ +@import 'forecasting_modal'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/index.js b/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/index.js index 1231d3f205d6f2b..7f8ea9db3b501be 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/index.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/forecasting_modal/index.js @@ -6,5 +6,4 @@ -import './forecasting_modal_directive'; -import './styles/main.less'; +import './forecasting_modal_directive'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/index.js b/x-pack/plugins/ml/public/timeseriesexplorer/index.js index 912b514c4b0c111..d291275bc9141d9 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/index.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/index.js @@ -10,6 +10,5 @@ import './forecasting_modal'; import './timeseriesexplorer_controller.js'; import './timeseries_search_service.js'; import './timeseries_chart_directive'; -import './styles/main.less'; import 'plugins/ml/components/job_select_list'; import 'plugins/ml/components/chart_tooltip'; diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart.js new file mode 100644 index 000000000000000..0c54cf9a91ae9ff --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart.js @@ -0,0 +1,1239 @@ +/* + * 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. + */ + + + +/* + * React component chart plotting data from a single time series, with or without model plot enabled, + * annotated with anomalies. + */ + + +import PropTypes from 'prop-types'; +import React from 'react'; + +import _ from 'lodash'; +import d3 from 'd3'; +import moment from 'moment'; + +import { + getSeverityWithLow, + getMultiBucketImpactLabel, +} from '../../common/util/anomaly_utils'; +import { formatValue } from '../formatters/format_value'; +import { + LINE_CHART_ANOMALY_RADIUS, + MULTI_BUCKET_SYMBOL_SIZE, + SCHEDULED_EVENT_SYMBOL_HEIGHT, + drawLineChartDots, + filterAxisLabels, + numTicksForDateFormat, + showMultiBucketAnomalyMarker, + showMultiBucketAnomalyTooltip, +} from '../util/chart_utils'; +import { TimeBuckets } from 'ui/time_buckets'; +import { mlAnomaliesTableService } from '../components/anomalies_table/anomalies_table_service'; +import ContextChartMask from './context_chart_mask'; +import { findChartPointForAnomalyTime } from './timeseriesexplorer_utils'; +import { mlEscape } from '../util/string_utils'; +import { mlFieldFormatService } from '../services/field_format_service'; +import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service'; + +const focusZoomPanelHeight = 25; +const focusChartHeight = 310; +const focusHeight = focusZoomPanelHeight + focusChartHeight; +const contextChartHeight = 60; +const contextChartLineTopMargin = 3; +const chartSpacing = 25; +const swimlaneHeight = 30; +const margin = { top: 20, right: 10, bottom: 15, left: 40 }; + +const ZOOM_INTERVAL_OPTIONS = [ + { duration: moment.duration(1, 'h'), label: '1h' }, + { duration: moment.duration(12, 'h'), label: '12h' }, + { duration: moment.duration(1, 'd'), label: '1d' }, + { duration: moment.duration(1, 'w'), label: '1w' }, + { duration: moment.duration(2, 'w'), label: '2w' }, + { duration: moment.duration(1, 'M'), label: '1M' }]; + +// Set up the color scale to use for indicating score. +const anomalyColorScale = d3.scale.threshold() + .domain([3, 25, 50, 75, 100]) + .range(['#d2e9f7', '#8bc8fb', '#ffdd00', '#ff7e00', '#fe5050']); + +// Create a gray-toned version of the color scale to use under the context chart mask. +const anomalyGrayScale = d3.scale.threshold() + .domain([3, 25, 50, 75, 100]) + .range(['#dce7ed', '#b0c5d6', '#b1a34e', '#b17f4e', '#c88686']); + +function getSvgHeight() { + return focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom; +} + +export class TimeseriesChart extends React.Component { + static propTypes = { + autoZoomDuration: PropTypes.number, + contextAggregationInterval: PropTypes.object, + contextChartData: PropTypes.array, + contextForecastData: PropTypes.array, + contextChartSelected: PropTypes.func, + detectorIndex: PropTypes.string, + focusAggregationInterval: PropTypes.object, + focusChartData: PropTypes.array, + focusForecastData: PropTypes.array, + modelPlotEnabled: PropTypes.bool, + renderFocusChartOnly: PropTypes.bool, + selectedJob: PropTypes.object, + showForecast: PropTypes.bool, + showModelBounds: PropTypes.bool, + svgWidth: PropTypes.number, + swimlaneData: PropTypes.array, + timefilter: PropTypes.object, + zoomFrom: PropTypes.object, + zoomTo: PropTypes.object + } + + componentWillUnmount() { + const element = d3.select(this.rootNode); + element.html(''); + + mlAnomaliesTableService.anomalyRecordMouseenter.unwatch(this.tableRecordMousenterListener); + mlAnomaliesTableService.anomalyRecordMouseleave.unwatch(this.tableRecordMouseleaveListener); + } + + componentDidMount() { + const { + svgWidth + } = this.props; + + this.vizWidth = svgWidth - margin.left - margin.right; + const vizWidth = this.vizWidth; + + this.focusXScale = d3.time.scale().range([0, vizWidth]); + this.focusYScale = d3.scale.linear().range([focusHeight, focusZoomPanelHeight]); + const focusXScale = this.focusXScale; + const focusYScale = this.focusYScale; + + this.focusXAxis = d3.svg.axis().scale(focusXScale).orient('bottom') + .innerTickSize(-focusChartHeight).outerTickSize(0).tickPadding(10); + this.focusYAxis = d3.svg.axis().scale(focusYScale).orient('left') + .innerTickSize(-vizWidth).outerTickSize(0).tickPadding(10); + + this.focusValuesLine = d3.svg.line() + .x(function (d) { return focusXScale(d.date); }) + .y(function (d) { return focusYScale(d.value); }) + .defined(d => d.value !== null); + this.focusBoundedArea = d3.svg.area() + .x(function (d) { return focusXScale(d.date) || 1; }) + .y0(function (d) { return focusYScale(d.upper); }) + .y1(function (d) { return focusYScale(d.lower); }) + .defined(d => (d.lower !== null && d.upper !== null)); + + this.contextXScale = d3.time.scale().range([0, vizWidth]); + this.contextYScale = d3.scale.linear().range([contextChartHeight, contextChartLineTopMargin]); + + this.fieldFormat = undefined; + + this.brush = d3.svg.brush(); + + this.mask = undefined; + + // Listeners for mouseenter/leave events for rows in the table + // to highlight the corresponding anomaly mark in the focus chart. + const highlightFocusChartAnomaly = this.highlightFocusChartAnomaly.bind(this); + this.tableRecordMousenterListener = function (record) { + highlightFocusChartAnomaly(record); + }; + + const unhighlightFocusChartAnomaly = this.unhighlightFocusChartAnomaly.bind(this); + this.tableRecordMouseleaveListener = function (record) { + unhighlightFocusChartAnomaly(record); + }; + + mlAnomaliesTableService.anomalyRecordMouseenter.watch(this.tableRecordMousenterListener); + mlAnomaliesTableService.anomalyRecordMouseleave.watch(this.tableRecordMouseleaveListener); + + this.renderChart(); + this.drawContextChartSelection(); + this.renderFocusChart(); + } + + componentDidUpdate() { + if (this.props.renderFocusChartOnly === false) { + this.renderChart(); + this.drawContextChartSelection(); + } + + this.renderFocusChart(); + } + + renderChart() { + const { + contextChartData, + contextForecastData, + detectorIndex, + modelPlotEnabled, + selectedJob, + svgWidth + } = this.props; + + const createFocusChart = this.createFocusChart.bind(this); + const drawContextElements = this.drawContextElements.bind(this); + const focusXScale = this.focusXScale; + const focusYAxis = this.focusYAxis; + const focusYScale = this.focusYScale; + + const svgHeight = getSvgHeight(); + + // Clear any existing elements from the visualization, + // then build the svg elements for the bubble chart. + const chartElement = d3.select(this.rootNode); + chartElement.selectAll('*').remove(); + + if (typeof selectedJob !== 'undefined') { + this.fieldFormat = mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); + } else { + return; + } + + if (contextChartData === undefined) { + return; + } + + const fieldFormat = this.fieldFormat; + + const svg = chartElement.append('svg') + .attr('width', svgWidth) + .attr('height', svgHeight); + + let contextDataMin; + let contextDataMax; + if (modelPlotEnabled === true || + (contextForecastData !== undefined && contextForecastData.length > 0)) { + const combinedData = contextForecastData === undefined ? + contextChartData : contextChartData.concat(contextForecastData); + + contextDataMin = d3.min(combinedData, d => Math.min(d.value, d.lower)); + contextDataMax = d3.max(combinedData, d => Math.max(d.value, d.upper)); + + } else { + contextDataMin = d3.min(contextChartData, d => d.value); + contextDataMax = d3.max(contextChartData, d => d.value); + } + + // Set the size of the left margin according to the width of the largest y axis tick label. + // The min / max of the aggregated context chart data may be less than the min / max of the + // data which is displayed in the focus chart which is likely to be plotted at a lower + // aggregation interval. Therefore ceil the min / max with the higher absolute value to allow + // for extra space for chart labels which may have higher values than the context data + // e.g. aggregated max may be 9500, whereas focus plot max may be 11234. + const ceiledMax = contextDataMax > 0 ? + Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMax)))) : contextDataMax; + + const flooredMin = contextDataMin >= 0 ? + contextDataMin : -1 * Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMin)))); + + // Temporarily set the domain of the focus y axis to the min / max of the full context chart + // data range so that we can measure the maximum tick label width on temporary text elements. + focusYScale.domain([flooredMin, ceiledMax]); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g') + .attr('class', 'temp-axis-label tick'); + tempLabelText.selectAll('text.temp.axis').data(focusYScale.ticks()) + .enter() + .append('text') + .text((d) => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + return focusYScale.tickFormat()(d); + } + }) + .each(function () { + maxYAxisLabelWidth = Math.max(this.getBBox().width + focusYAxis.tickPadding(), maxYAxisLabelWidth); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + margin.left = (Math.max(maxYAxisLabelWidth, 40)); + this.vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); + focusXScale.range([0, this.vizWidth]); + focusYAxis.innerTickSize(-this.vizWidth); + + const focus = svg.append('g') + .attr('class', 'focus-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + const context = svg.append('g') + .attr('class', 'context-chart') + .attr('transform', 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')'); + + // Draw each of the component elements. + createFocusChart(focus, this.vizWidth, focusHeight); + drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight); + } + + drawContextChartSelection() { + const { + contextChartData, + contextChartSelected, + contextForecastData, + zoomFrom, + zoomTo + } = this.props; + + if (contextChartData === undefined) { + return; + } + + const setContextBrushExtent = this.setContextBrushExtent.bind(this); + + // Make appropriate selection in the context chart to trigger loading of the focus chart. + let focusLoadFrom; + let focusLoadTo; + const contextXMin = this.contextXScale.domain()[0].getTime(); + const contextXMax = this.contextXScale.domain()[1].getTime(); + + let combinedData = contextChartData; + if (contextForecastData !== undefined) { + combinedData = combinedData.concat(contextForecastData); + } + + if (zoomFrom) { + focusLoadFrom = zoomFrom.getTime(); + } else { + focusLoadFrom = _.reduce(combinedData, (memo, point) => + Math.min(memo, point.date.getTime()), new Date(2099, 12, 31).getTime()); + } + focusLoadFrom = Math.max(focusLoadFrom, contextXMin); + + if (zoomTo) { + focusLoadTo = zoomTo.getTime(); + } else { + focusLoadTo = _.reduce(combinedData, (memo, point) => Math.max(memo, point.date.getTime()), 0); + } + focusLoadTo = Math.min(focusLoadTo, contextXMax); + + if ((focusLoadFrom !== contextXMin) || (focusLoadTo !== contextXMax)) { + setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); + } else { + // Don't set the brush if the selection is the full context chart domain. + this.setBrushVisibility(false); + const selectedBounds = this.contextXScale.domain(); + this.selectedBounds = { min: moment(new Date(selectedBounds[0])), max: moment(selectedBounds[1]) }; + contextChartSelected({ from: selectedBounds[0], to: selectedBounds[1] }); + } + } + + createFocusChart(fcsGroup, fcsWidth, fcsHeight) { + // Split out creation of the focus chart from the rendering, + // as we want to re-render the paths and points when the zoom area changes. + + const { + contextForecastData + } = this.props; + + // Add a group at the top to display info on the chart aggregation interval + // and links to set the brush span to 1h, 1d, 1w etc. + const zoomGroup = fcsGroup.append('g') + .attr('class', 'focus-zoom'); + zoomGroup.append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', fcsWidth) + .attr('height', focusZoomPanelHeight) + .attr('class', 'chart-border'); + this.createZoomInfoElements(zoomGroup, fcsWidth); + + // Add border round plot area. + fcsGroup.append('rect') + .attr('x', 0) + .attr('y', focusZoomPanelHeight) + .attr('width', fcsWidth) + .attr('height', focusChartHeight) + .attr('class', 'chart-border'); + + // Add background for x axis. + const xAxisBg = fcsGroup.append('g') + .attr('class', 'x-axis-background'); + xAxisBg.append('rect') + .attr('x', 0) + .attr('y', fcsHeight) + .attr('width', fcsWidth) + .attr('height', chartSpacing); + xAxisBg.append('line') + .attr('x1', 0) + .attr('y1', fcsHeight) + .attr('x2', 0) + .attr('y2', fcsHeight + chartSpacing); + xAxisBg.append('line') + .attr('x1', fcsWidth) + .attr('y1', fcsHeight) + .attr('x2', fcsWidth) + .attr('y2', fcsHeight + chartSpacing); + xAxisBg.append('line') + .attr('x1', 0) + .attr('y1', fcsHeight + chartSpacing) + .attr('x2', fcsWidth) + .attr('y2', fcsHeight + chartSpacing); + + const axes = fcsGroup.append('g'); + axes.append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + fcsHeight + ')'); + axes.append('g') + .attr('class', 'y axis'); + + // Create the elements for the metric value line and model bounds area. + fcsGroup.append('path') + .attr('class', 'area bounds'); + fcsGroup.append('path') + .attr('class', 'values-line'); + fcsGroup.append('g') + .attr('class', 'focus-chart-markers'); + + + // Create the path elements for the forecast value line and bounds area. + if (contextForecastData) { + fcsGroup.append('path') + .attr('class', 'area forecast'); + fcsGroup.append('path') + .attr('class', 'values-line forecast'); + fcsGroup.append('g') + .attr('class', 'focus-chart-markers forecast'); + } + + fcsGroup.append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', fcsWidth) + .attr('height', fcsHeight + 24) + .attr('class', 'chart-border chart-border-highlight'); + } + + renderFocusChart() { + const { + focusAggregationInterval, + focusChartData, + focusForecastData, + modelPlotEnabled, + selectedJob, + showForecast, + showModelBounds + } = this.props; + + if (focusChartData === undefined) { + return; + } + + const data = focusChartData; + + const contextYScale = this.contextYScale; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + + const focusChart = d3.select('.focus-chart'); + + // Update the plot interval labels. + const focusAggInt = focusAggregationInterval.expression; + const bucketSpan = selectedJob.analysis_config.bucket_span; + const chartElement = d3.select(this.rootNode); + chartElement.select('.zoom-aggregation-interval').text( + `(aggregation interval: ${focusAggInt}, bucket span: ${bucketSpan})`); + + // Render the axes. + + // Calculate the x axis domain. + // Elasticsearch aggregation returns points at start of bucket, + // so set the x-axis min to the start of the first aggregation interval, + // and the x-axis max to the end of the last aggregation interval. + const bounds = this.selectedBounds; + if (typeof bounds === 'undefined') { + return; + } + const aggMs = focusAggregationInterval.asMilliseconds(); + const earliest = moment(Math.floor((bounds.min.valueOf()) / aggMs) * aggMs); + const latest = moment(Math.ceil((bounds.max.valueOf()) / aggMs) * aggMs); + this.focusXScale.domain([earliest.toDate(), latest.toDate()]); + + // Calculate the y-axis domain. + if (focusChartData.length > 0 || + (focusForecastData !== undefined && focusForecastData.length > 0)) { + if (this.fieldFormat !== undefined) { + this.focusYAxis.tickFormat(d => this.fieldFormat.convert(d, 'text')); + } else { + // Use default tick formatter. + this.focusYAxis.tickFormat(null); + } + + // Calculate the min/max of the metric data and the forecast data. + let yMin = 0; + let yMax = 0; + + let combinedData = data; + if (focusForecastData !== undefined && focusForecastData.length > 0) { + combinedData = data.concat(focusForecastData); + } + + yMin = d3.min(combinedData, (d) => { + return d.lower !== undefined ? Math.min(d.value, d.lower) : d.value; + }); + yMax = d3.max(combinedData, (d) => { + return d.upper !== undefined ? Math.max(d.value, d.upper) : d.value; + }); + + if (yMax === yMin) { + if ( + this.contextYScale.domain()[0] !== contextYScale.domain()[1] && + yMin >= contextYScale.domain()[0] && yMax <= contextYScale.domain()[1] + ) { + // Set the focus chart limits to be the same as the context chart. + yMin = contextYScale.domain()[0]; + yMax = contextYScale.domain()[1]; + } else { + yMin -= (yMin * 0.05); + yMax += (yMax * 0.05); + } + } + + this.focusYScale.domain([yMin, yMax]); + + } else { + // Display 10 unlabelled ticks. + this.focusYScale.domain([0, 10]); + this.focusYAxis.tickFormat(''); + } + + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval('auto'); + timeBuckets.setBounds(bounds); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + focusChart.select('.x.axis') + .call(this.focusXAxis.ticks(numTicksForDateFormat(this.vizWidth), xAxisTickFormat) + .tickFormat((d) => { + return moment(d).format(xAxisTickFormat); + })); + focusChart.select('.y.axis') + .call(this.focusYAxis); + + filterAxisLabels(focusChart.select('.x.axis'), this.vizWidth); + + // Render the bounds area and values line. + if (modelPlotEnabled === true) { + focusChart.select('.area.bounds') + .attr('d', this.focusBoundedArea(data)) + .classed('hidden', !showModelBounds); + } + + focusChart.select('.values-line') + .attr('d', this.focusValuesLine(data)); + drawLineChartDots(data, focusChart, this.focusValuesLine); + + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps) or for multi-bucket anomalies. + const dots = d3.select('.focus-chart-markers').selectAll('.metric-value') + .data(data.filter(d => (d.value !== null && !showMultiBucketAnomalyMarker(d)))); + + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots.enter().append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .on('mouseover', function (d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all dots to new positions. + dots.attr('cx', (d) => { return this.focusXScale(d.date); }) + .attr('cy', (d) => { return this.focusYScale(d.value); }) + .attr('class', (d) => { + let markerClass = 'metric-value'; + if (_.has(d, 'anomalyScore')) { + markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore)}`; + } + return markerClass; + }); + + + // Render cross symbols for any multi-bucket anomalies. + const multiBucketMarkers = d3.select('.focus-chart-markers').selectAll('.multi-bucket') + .data(data.filter(d => (d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true))); + + // Remove multi-bucket markers that are no longer needed. + multiBucketMarkers.exit().remove(); + + // Add any new markers that are needed i.e. if number of multi-bucket points has increased. + multiBucketMarkers.enter().append('path') + .attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross')) + .on('mouseover', function (d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all markers to new positions. + multiBucketMarkers.attr('transform', d => `translate(${this.focusXScale(d.date)}, ${this.focusYScale(d.value)})`) + .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)}`); + + + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = d3.select('.focus-chart-markers').selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers.enter().append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // Update all markers to new positions. + scheduledEventMarkers.attr('x', (d) => this.focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) + .attr('y', (d) => this.focusYScale(d.value) - 3); + + // Plot any forecast data in scope. + if (focusForecastData !== undefined) { + focusChart.select('.area.forecast') + .attr('d', this.focusBoundedArea(focusForecastData)) + .classed('hidden', !showForecast); + focusChart.select('.values-line.forecast') + .attr('d', this.focusValuesLine(focusForecastData)) + .classed('hidden', !showForecast); + + const forecastDots = d3.select('.focus-chart-markers.forecast').selectAll('.metric-value') + .data(focusForecastData); + + // Remove dots that are no longer needed i.e. if number of forecast points has decreased. + forecastDots.exit().remove(); + // Create any new dots that are needed i.e. if number of forecast points has increased. + forecastDots.enter().append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .on('mouseover', function (d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all dots to new positions. + forecastDots.attr('cx', (d) => { return this.focusXScale(d.date); }) + .attr('cy', (d) => { return this.focusYScale(d.value); }) + .attr('class', 'metric-value') + .classed('hidden', !showForecast); + } + + } + + createZoomInfoElements(zoomGroup, fcsWidth) { + const { + autoZoomDuration, + modelPlotEnabled, + timefilter + } = this.props; + + const setZoomInterval = this.setZoomInterval.bind(this); + + // Create zoom duration links applicable for the current time span. + // Don't add links for any durations which would give a brush extent less than 10px. + const bounds = timefilter.getActiveBounds(); + const boundsSecs = bounds.max.unix() - bounds.min.unix(); + const minSecs = (10 / this.vizWidth) * boundsSecs; + + let xPos = 10; + const zoomLabel = zoomGroup.append('text') + .attr('x', xPos) + .attr('y', 17) + .attr('class', 'zoom-info-text') + .text('Zoom:'); + + const zoomOptions = [{ durationMs: autoZoomDuration, label: 'auto' }]; + _.each(ZOOM_INTERVAL_OPTIONS, (option) => { + if (option.duration.asSeconds() > minSecs && + option.duration.asSeconds() < boundsSecs) { + zoomOptions.push({ durationMs: option.duration.asMilliseconds(), label: option.label }); + } + }); + xPos += (zoomLabel.node().getBBox().width + 4); + + _.each(zoomOptions, (option) => { + const text = zoomGroup.append('a') + .attr('data-ms', option.durationMs) + .attr('href', '') + .append('text') + .attr('x', xPos) + .attr('y', 17) + .attr('class', 'zoom-info-text') + .text(option.label); + + xPos += (text.node().getBBox().width + 4); + }); + + zoomGroup.append('text') + .attr('x', (xPos + 6)) + .attr('y', 17) + .attr('class', 'zoom-info-text zoom-aggregation-interval') + .text('(aggregation interval: , bucket span: )'); + + if (modelPlotEnabled === false) { + const modelPlotLabel = zoomGroup.append('text') + .attr('x', 300) + .attr('y', 17) + .attr('class', 'zoom-info-text') + .text('Model bounds are not available'); + + modelPlotLabel.attr('x', (fcsWidth - (modelPlotLabel.node().getBBox().width + 10))); + } + + const chartElement = d3.select(this.rootNode); + chartElement.selectAll('.focus-zoom a').on('click', function () { + d3.event.preventDefault(); + setZoomInterval(d3.select(this).attr('data-ms')); + }); + } + + drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { + const { + contextChartData, + contextForecastData, + modelPlotEnabled, + timefilter + } = this.props; + + const data = contextChartData; + + const calculateContextXAxisDomain = this.calculateContextXAxisDomain.bind(this); + const drawContextBrush = this.drawContextBrush.bind(this); + const drawSwimlane = this.drawSwimlane.bind(this); + + this.contextXScale = d3.time.scale().range([0, cxtWidth]) + .domain(calculateContextXAxisDomain()); + + const combinedData = contextForecastData === undefined ? data : data.concat(contextForecastData); + const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; + _.each(combinedData, (item) => { + valuesRange.min = Math.min(item.value, valuesRange.min); + valuesRange.max = Math.max(item.value, valuesRange.max); + }); + let dataMin = valuesRange.min; + let dataMax = valuesRange.max; + const chartLimits = { min: dataMin, max: dataMax }; + + if (modelPlotEnabled === true || + (contextForecastData !== undefined && contextForecastData.length > 0)) { + const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; + _.each(combinedData, (item) => { + boundsRange.min = Math.min(item.lower, boundsRange.min); + boundsRange.max = Math.max(item.upper, boundsRange.max); + }); + dataMin = Math.min(dataMin, boundsRange.min); + dataMax = Math.max(dataMax, boundsRange.max); + + // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. + if ((valuesRange.max - valuesRange.min) < 0.5 * (dataMax - dataMin)) { + if (valuesRange.min > dataMin) { + chartLimits.min = valuesRange.min - (0.5 * (valuesRange.max - valuesRange.min)); + } + + if (valuesRange.max < dataMax) { + chartLimits.max = valuesRange.max + (0.5 * (valuesRange.max - valuesRange.min)); + } + } + } + + this.contextYScale = d3.scale.linear().range([cxtChartHeight, contextChartLineTopMargin]) + .domain([chartLimits.min, chartLimits.max]); + + const borders = cxtGroup.append('g') + .attr('class', 'axis'); + + // Add borders left and right. + borders.append('line') + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', 0) + .attr('y2', cxtChartHeight + swlHeight); + borders.append('line') + .attr('x1', cxtWidth) + .attr('y1', 0) + .attr('x2', cxtWidth) + .attr('y2', cxtChartHeight + swlHeight); + + // Add x axis. + const bounds = timefilter.getActiveBounds(); + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval('auto'); + timeBuckets.setBounds(bounds); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + const xAxis = d3.svg.axis().scale(this.contextXScale) + .orient('top') + .innerTickSize(-cxtChartHeight) + .outerTickSize(0) + .tickPadding(0) + .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) + .tickFormat((d) => { + return moment(d).format(xAxisTickFormat); + }); + + cxtGroup.datum(data); + + const contextBoundsArea = d3.svg.area() + .x((d) => { return this.contextXScale(d.date); }) + .y0((d) => { return this.contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); }) + .y1((d) => { return this.contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); }) + .defined(d => (d.lower !== null && d.upper !== null)); + + if (modelPlotEnabled === true) { + cxtGroup.append('path') + .datum(data) + .attr('class', 'area context') + .attr('d', contextBoundsArea); + } + + const contextValuesLine = d3.svg.line() + .x((d) => { return this.contextXScale(d.date); }) + .y((d) => { return this.contextYScale(d.value); }) + .defined(d => d.value !== null); + + cxtGroup.append('path') + .datum(data) + .attr('class', 'values-line') + .attr('d', contextValuesLine); + drawLineChartDots(data, cxtGroup, contextValuesLine, 1); + + // Create the path elements for the forecast value line and bounds area. + if (contextForecastData !== undefined) { + cxtGroup.append('path') + .datum(contextForecastData) + .attr('class', 'area forecast') + .attr('d', contextBoundsArea); + cxtGroup.append('path') + .datum(contextForecastData) + .attr('class', 'values-line forecast') + .attr('d', contextValuesLine); + } + + // Create and draw the anomaly swimlane. + const swimlane = cxtGroup.append('g') + .attr('class', 'swimlane') + .attr('transform', 'translate(0,' + cxtChartHeight + ')'); + + drawSwimlane(swimlane, cxtWidth, swlHeight); + + // Draw a mask over the sections of the context chart and swimlane + // which fall outside of the zoom brush selection area. + this.mask = new ContextChartMask(cxtGroup, contextChartData, modelPlotEnabled, swlHeight) + .x(this.contextXScale) + .y(this.contextYScale); + + // Draw the x axis on top of the mask so that the labels are visible. + cxtGroup.append('g') + .attr('class', 'x axis context-chart-axis') + .call(xAxis); + + // Move the x axis labels up so that they are inside the contact chart area. + cxtGroup.selectAll('.x.context-chart-axis text') + .attr('dy', (cxtChartHeight - 5)); + + filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); + + drawContextBrush(cxtGroup); + } + + drawContextBrush(contextGroup) { + const { + contextChartSelected + } = this.props; + + const brush = this.brush; + const contextXScale = this.contextXScale; + const setBrushVisibility = this.setBrushVisibility.bind(this); + const mask = this.mask; + + // Create the brush for zooming in to the focus area of interest. + brush.x(contextXScale) + .on('brush', brushing) + .on('brushend', brushed); + + contextGroup.append('g') + .attr('class', 'x brush') + .call(brush) + .selectAll('rect') + .attr('y', -1) + .attr('height', contextChartHeight + swimlaneHeight + 1); + + // move the left and right resize areas over to + // be under the handles + contextGroup.selectAll('.w rect') + .attr('x', -10) + .attr('width', 10); + + contextGroup.selectAll('.e rect') + .attr('x', 0) + .attr('width', 10); + + const topBorder = contextGroup.append('rect') + .attr('class', 'top-border') + .attr('y', -2) + .attr('height', contextChartLineTopMargin); + + // Draw the brush handles using SVG foreignObject elements. + // Note these are not supported on IE11 and below, so will not appear in IE. + const leftHandle = contextGroup.append('foreignObject') + .attr('width', 10) + .attr('height', 90) + .attr('class', 'brush-handle') + .html('
'); + const rightHandle = contextGroup.append('foreignObject') + .attr('width', 10) + .attr('height', 90) + .attr('class', 'brush-handle') + .html('
'); + + setBrushVisibility(!brush.empty()); + + function showBrush(show) { + if (show === true) { + const brushExtent = brush.extent(); + mask.reveal(brushExtent); + leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); + rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); + + topBorder.attr('x', contextXScale(brushExtent[0]) + 1); + topBorder.attr('width', contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2); + } + + setBrushVisibility(show); + } + + function brushing() { + const isEmpty = brush.empty(); + showBrush(!isEmpty); + } + + const that = this; + function brushed() { + const isEmpty = brush.empty(); + showBrush(!isEmpty); + + const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); + const selectionMin = selectedBounds[0].getTime(); + const selectionMax = selectedBounds[1].getTime(); + + // Set the color of the swimlane cells according to whether they are inside the selection. + contextGroup.selectAll('.swimlane-cell') + .style('fill', (d) => { + const cellMs = d.date.getTime(); + if (cellMs < selectionMin || cellMs > selectionMax) { + return anomalyGrayScale(d.score); + } else { + return anomalyColorScale(d.score); + } + }); + + that.selectedBounds = { min: moment(selectionMin), max: moment(selectionMax) }; + contextChartSelected({ from: selectedBounds[0], to: selectedBounds[1] }); + } + } + + setBrushVisibility(show) { + const mask = this.mask; + + if (mask !== undefined) { + const visibility = show ? 'visible' : 'hidden'; + mask.style('visibility', visibility); + + d3.selectAll('.brush').style('visibility', visibility); + + const brushHandles = d3.selectAll('.brush-handle-inner'); + brushHandles.style('visibility', visibility); + + const topBorder = d3.selectAll('.top-border'); + topBorder.style('visibility', visibility); + + const border = d3.selectAll('.chart-border-highlight'); + border.style('visibility', visibility); + } + } + + drawSwimlane(swlGroup, swlWidth, swlHeight) { + const { + contextAggregationInterval, + swimlaneData + } = this.props; + + const calculateContextXAxisDomain = this.calculateContextXAxisDomain.bind(this); + + const data = swimlaneData; + + if (typeof data === 'undefined') { + return; + } + + // Calculate the x axis domain. + // Elasticsearch aggregation returns points at start of bucket, so set the + // x-axis min to the start of the aggregation interval. + // Need to use the min(earliest) and max(earliest) of the context chart + // aggregation to align the axes of the chart and swimlane elements. + const xAxisDomain = calculateContextXAxisDomain(); + const x = d3.time.scale().range([0, swlWidth]) + .domain(xAxisDomain); + + const y = d3.scale.linear().range([swlHeight, 0]) + .domain([0, swlHeight]); + + const xAxis = d3.svg.axis() + .scale(x) + .orient('bottom') + .innerTickSize(-swlHeight) + .outerTickSize(0); + + const yAxis = d3.svg.axis() + .scale(y) + .orient('left') + .tickValues(y.domain()) + .innerTickSize(-swlWidth) + .outerTickSize(0); + + const axes = swlGroup.append('g'); + + axes.append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + (swlHeight) + ')') + .call(xAxis); + + axes.append('g') + .attr('class', 'y axis') + .call(yAxis); + + const earliest = xAxisDomain[0].getTime(); + const latest = xAxisDomain[1].getTime(); + const swimlaneAggMs = contextAggregationInterval.asMilliseconds(); + let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); + if (cellWidth < 1) { + cellWidth = 1; + } + + const cells = swlGroup.append('g') + .attr('class', 'swimlane-cells') + .selectAll('rect') + .data(data); + + cells.enter().append('rect') + .attr('x', (d) => { return x(d.date); }) + .attr('y', 0) + .attr('rx', 0) + .attr('ry', 0) + .attr('class', (d) => { return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden'; }) + .attr('width', cellWidth) + .attr('height', swlHeight) + .style('fill', (d) => { return anomalyColorScale(d.score); }); + + } + + calculateContextXAxisDomain() { + const { + contextAggregationInterval, + swimlaneData, + timefilter + } = this.props; + // Calculates the x axis domain for the context elements. + // Elasticsearch aggregation returns points at start of bucket, + // so set the x-axis min to the start of the first aggregation interval, + // and the x-axis max to the end of the last aggregation interval. + // Context chart and swimlane use the same aggregation interval. + const bounds = timefilter.getActiveBounds(); + let earliest = bounds.min.valueOf(); + + if (swimlaneData !== undefined && swimlaneData.length > 0) { + // Adjust the earliest back to the time of the first swimlane point + // if this is before the time filter minimum. + earliest = Math.min(_.first(swimlaneData).date.getTime(), bounds.min.valueOf()); + } + + const contextAggMs = contextAggregationInterval.asMilliseconds(); + const earliestMs = Math.floor(earliest / contextAggMs) * contextAggMs; + const latestMs = Math.ceil((bounds.max.valueOf()) / contextAggMs) * contextAggMs; + + return [new Date(earliestMs), new Date(latestMs)]; + } + + // Sets the extent of the brush on the context chart to the + // supplied from and to Date objects. + setContextBrushExtent(from, to, fireEvent) { + const brush = this.brush; + brush.extent([from, to]); + brush(d3.select('.brush')); + if (fireEvent) { + brush.event(d3.select('.brush')); + } + } + + setZoomInterval(ms) { + const { + timefilter, + zoomTo + } = this.props; + + const setContextBrushExtent = this.setContextBrushExtent.bind(this); + + const bounds = timefilter.getActiveBounds(); + const minBoundsMs = bounds.min.valueOf(); + const maxBoundsMs = bounds.max.valueOf(); + + // Attempt to retain the same zoom end time. + // If not, go back to the bounds start and add on the required millis. + const millis = +ms; + let to = zoomTo.getTime(); + let from = to - millis; + if (from < minBoundsMs) { + from = minBoundsMs; + to = Math.min(minBoundsMs + millis, maxBoundsMs); + } + + setContextBrushExtent(new Date(from), new Date(to), true); + } + + showFocusChartTooltip(marker, circle) { + const { + modelPlotEnabled + } = this.props; + + const fieldFormat = this.fieldFormat; + + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = moment(marker.date).format('MMMM Do YYYY, HH:mm'); + let contents = formattedDate + '

'; + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = (score > 0 ? score : '< 1'); + contents += `anomaly score: ${displayScore}
`; + + if (showMultiBucketAnomalyTooltip(marker) === true) { + contents += `multi-bucket impact: ${getMultiBucketImpactLabel(marker.multiBucketImpact)}
`; + } + + if (modelPlotEnabled === false) { + // Show actual/typical when available except for rare detectors. + // Rare detectors always have 1 as actual and the probability as typical. + // Exposing those values in the tooltip with actual/typical labels might irritate users. + if (_.has(marker, 'actual') && marker.function !== 'rare') { + // Display the record actual in preference to the chart value, which may be + // different depending on the aggregation interval of the chart. + contents += `actual: ${formatValue(marker.actual, marker.function, fieldFormat)}`; + contents += `
typical: ${formatValue(marker.typical, marker.function, fieldFormat)}`; + } else { + contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; + if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { + const numberOfCauses = marker.numberOfCauses; + const byFieldName = mlEscape(marker.byFieldName); + if (numberOfCauses < 10) { + // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. + contents += `
${numberOfCauses} unusual ${byFieldName} values`; + } else { + // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. + contents += `
${numberOfCauses}+ unusual ${byFieldName} values`; + } + } + } + } else { + contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; + contents += `
upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`; + contents += `
lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`; + } + } else { + // TODO - need better formatting for small decimals. + if (_.get(marker, 'isForecast', false) === true) { + contents += `prediction: ${formatValue(marker.value, marker.function, fieldFormat)}`; + } else { + contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; + } + + if (modelPlotEnabled === true) { + contents += `
upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`; + contents += `
lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`; + } + } + + if (_.has(marker, 'scheduledEvents')) { + contents += `

Scheduled events:
${marker.scheduledEvents.map(mlEscape).join('
')}`; + } + + mlChartTooltipService.show(contents, circle, { + x: LINE_CHART_ANOMALY_RADIUS * 2, + y: 0 + }); + } + + highlightFocusChartAnomaly(record) { + // Highlights the anomaly marker in the focus chart corresponding to the specified record. + + const { + focusChartData + } = this.props; + + const focusXScale = this.focusXScale; + const focusYScale = this.focusYScale; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + + // Find the anomaly marker which corresponds to the time of the anomaly record. + // Depending on the way the chart is aggregated, there may not be + // a point at exactly the same time as the record being highlighted. + const anomalyTime = record.source.timestamp; + const markerToSelect = findChartPointForAnomalyTime(focusChartData, anomalyTime); + + // Render an additional highlighted anomaly marker on the focus chart. + // TODO - plot anomaly markers for cases where there is an anomaly due + // to the absence of data and model plot is enabled. + if (markerToSelect !== undefined) { + const selectedMarker = d3.select('.focus-chart-markers') + .selectAll('.focus-chart-highlighted-marker') + .data([markerToSelect]); + if (showMultiBucketAnomalyMarker(markerToSelect) === true) { + selectedMarker.enter().append('path') + .attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross')) + .attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) + .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)} highlighted`); + } else { + selectedMarker.enter().append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .attr('cx', d => focusXScale(d.date)) + .attr('cy', d => focusYScale(d.value)) + .attr('class', d => `anomaly-marker metric-value ${getSeverityWithLow(d.anomalyScore)} highlighted`); + } + + // Display the chart tooltip for this marker. + // Note the values of the record and marker may differ depending on the levels of aggregation. + const chartElement = d3.select(this.rootNode); + const anomalyMarker = chartElement.selectAll('.focus-chart-markers .anomaly-marker.highlighted'); + if (anomalyMarker.length) { + showFocusChartTooltip(markerToSelect, anomalyMarker[0][0]); + } + } + } + + unhighlightFocusChartAnomaly() { + d3.select('.focus-chart-markers').selectAll('.anomaly-marker.highlighted').remove(); + mlChartTooltipService.hide(); + } + + shouldComponentUpdate() { + return true; + } + + setRef(componentNode) { + this.rootNode = componentNode; + } + + render() { + return
; + } +} diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart.test.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart.test.js new file mode 100644 index 000000000000000..5f306eb06ee88eb --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart.test.js @@ -0,0 +1,69 @@ +/* + * 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 mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; + +import moment from 'moment-timezone'; +import { mount } from 'enzyme'; +import React from 'react'; + +import { TimeseriesChart } from './timeseries_chart'; + +// mocking the following files because they import some core kibana +// code which the jest setup isn't happy with. +jest.mock('ui/chrome', () => ({ + getBasePath: path => path, + getUiSettingsClient: () => ({ + get: jest.fn() + }), +})); + +jest.mock('ui/time_buckets', () => ({ + TimeBuckets: function () { + this.setBounds = jest.fn(); + this.setInterval = jest.fn(); + this.getScaledDateFormat = jest.fn(); + } +})); + +jest.mock('../services/field_format_service', () => ({ + mlFieldFormatService: {} +})); + +function getTimeseriesChartPropsMock() { + return { + contextChartSelected: jest.fn(), + modelPlotEnabled: false, + showForecast: true, + showModelBounds: true, + svgWidth: 1600, + timefilter: {} + }; +} + +describe('TimeseriesChart', () => { + const mockedGetBBox = { x: 0, y: -10, width: 40, height: 20 }; + const originalGetBBox = SVGElement.prototype.getBBox; + beforeEach(() => { + moment.tz.setDefault('UTC'); + SVGElement.prototype.getBBox = () => mockedGetBBox; + }); + afterEach(() => { + moment.tz.setDefault('Browser'); + SVGElement.prototype.getBBox = originalGetBBox; + }); + + test('Minimal initialization', () => { + const props = getTimeseriesChartPropsMock(); + + const wrapper = mount(); + + expect(wrapper.html()).toBe( + `
` + ); + }); + +}); diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js index 1c212a4a4dd81ef..45c5437fc9ee75f 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js @@ -7,112 +7,75 @@ /* - * Chart showing plot time series data, with or without model plot enabled, + * Chart plotting data from a single time series, with or without model plot enabled, * annotated with anomalies. */ -import _ from 'lodash'; -import $ from 'jquery'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { TimeseriesChart } from './timeseries_chart'; + import angular from 'angular'; -import d3 from 'd3'; -import moment from 'moment'; import { timefilter } from 'ui/timefilter'; import { ResizeChecker } from 'ui/resize_checker'; -import { - getSeverityWithLow, - getMultiBucketImpactLabel, -} from 'plugins/ml/../common/util/anomaly_utils'; -import { formatValue } from 'plugins/ml/formatters/format_value'; -import { - LINE_CHART_ANOMALY_RADIUS, - MULTI_BUCKET_SYMBOL_SIZE, - SCHEDULED_EVENT_SYMBOL_HEIGHT, - drawLineChartDots, - filterAxisLabels, - numTicksForDateFormat, - showMultiBucketAnomalyMarker, - showMultiBucketAnomalyTooltip, -} from 'plugins/ml/util/chart_utils'; -import { TimeBuckets } from 'ui/time_buckets'; -import { mlAnomaliesTableService } from 'plugins/ml/components/anomalies_table/anomalies_table_service'; -import ContextChartMask from 'plugins/ml/timeseriesexplorer/context_chart_mask'; -import { findChartPointForAnomalyTime } from 'plugins/ml/timeseriesexplorer/timeseriesexplorer_utils'; -import { mlEscape } from 'plugins/ml/util/string_utils'; -import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; -import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service'; - import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); module.directive('mlTimeseriesChart', function () { function link(scope, element) { - // Key dimensions for the viz and constituent charts. let svgWidth = angular.element('.results-container').width(); - const focusZoomPanelHeight = 25; - const focusChartHeight = 310; - const focusHeight = focusZoomPanelHeight + focusChartHeight; - const contextChartHeight = 60; - const contextChartLineTopMargin = 3; - const chartSpacing = 25; - const swimlaneHeight = 30; - const margin = { top: 20, right: 10, bottom: 15, left: 40 }; - const svgHeight = focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom; - let vizWidth = svgWidth - margin.left - margin.right; - - const ZOOM_INTERVAL_OPTIONS = [ - { duration: moment.duration(1, 'h'), label: '1h' }, - { duration: moment.duration(12, 'h'), label: '12h' }, - { duration: moment.duration(1, 'd'), label: '1d' }, - { duration: moment.duration(1, 'w'), label: '1w' }, - { duration: moment.duration(2, 'w'), label: '2w' }, - { duration: moment.duration(1, 'M'), label: '1M' }]; - - // Set up the color scale to use for indicating score. - const anomalyColorScale = d3.scale.threshold() - .domain([3, 25, 50, 75, 100]) - .range(['#d2e9f7', '#8bc8fb', '#ffdd00', '#ff7e00', '#fe5050']); - - // Create a gray-toned version of the color scale to use under the context chart mask. - const anomalyGrayScale = d3.scale.threshold() - .domain([3, 25, 50, 75, 100]) - .range(['#dce7ed', '#b0c5d6', '#b1a34e', '#b17f4e', '#c88686']); - const focusXScale = d3.time.scale().range([0, vizWidth]); - const focusYScale = d3.scale.linear().range([focusHeight, focusZoomPanelHeight]); - - const focusXAxis = d3.svg.axis().scale(focusXScale).orient('bottom') - .innerTickSize(-focusChartHeight).outerTickSize(0).tickPadding(10); - const focusYAxis = d3.svg.axis().scale(focusYScale).orient('left') - .innerTickSize(-vizWidth).outerTickSize(0).tickPadding(10); - - const focusValuesLine = d3.svg.line() - .x(function (d) { return focusXScale(d.date); }) - .y(function (d) { return focusYScale(d.value); }) - .defined(d => d.value !== null); - const focusBoundedArea = d3.svg.area() - .x (function (d) { return focusXScale(d.date) || 1; }) - .y0(function (d) { return focusYScale(d.upper); }) - .y1(function (d) { return focusYScale(d.lower); }) - .defined(d => (d.lower !== null && d.upper !== null)); + function contextChartSelected(selection) { + scope.$root.$broadcast('contextChartSelected', selection); + } - let contextXScale = d3.time.scale().range([0, vizWidth]); - let contextYScale = d3.scale.linear().range([contextChartHeight, contextChartLineTopMargin]); + function renderReactComponent(renderFocusChartOnly = false) { + // Set the size of the components according to the width of the parent container at render time. + svgWidth = Math.max(angular.element('.results-container').width(), 0); - let fieldFormat = undefined; + const props = { + autoZoomDuration: scope.autoZoomDuration, + contextAggregationInterval: scope.contextAggregationInterval, + contextChartData: scope.contextChartData, + contextForecastData: scope.contextForecastData, + contextChartSelected: contextChartSelected, + detectorIndex: scope.detectorIndex, + focusChartData: scope.focusChartData, + focusForecastData: scope.focusForecastData, + focusAggregationInterval: scope.focusAggregationInterval, + modelPlotEnabled: scope.modelPlotEnabled, + renderFocusChartOnly, + selectedJob: scope.selectedJob, + showForecast: scope.showForecast, + showModelBounds: scope.showModelBounds, + svgWidth, + swimlaneData: scope.swimlaneData, + timefilter, + zoomFrom: scope.zoomFrom, + zoomTo: scope.zoomTo + }; + + ReactDOM.render( + React.createElement(TimeseriesChart, props), + element[0] + ); + } - const brush = d3.svg.brush(); - let mask; + renderReactComponent(); scope.$on('render', () => { - fieldFormat = mlFieldFormatService.getFieldFormat(scope.selectedJob.job_id, scope.detectorIndex); - render(); - drawContextChartSelection(); + renderReactComponent(); }); + function renderFocusChart() { + renderReactComponent(true); + } + scope.$watchCollection('focusForecastData', renderFocusChart); scope.$watchCollection('focusChartData', renderFocusChart); scope.$watchGroup(['showModelBounds', 'showForecast'], renderFocusChart); @@ -121,969 +84,15 @@ module.directive('mlTimeseriesChart', function () { const resizeChecker = new ResizeChecker(angular.element('.ml-timeseries-chart')); resizeChecker.on('resize', () => { scope.$evalAsync(() => { - // Wait a digest cycle before rendering to prevent - // the underlying ResizeObserver going into an infinite loop. - render(); - drawContextChartSelection(); - renderFocusChart(); + renderReactComponent(); }); }); - // Listeners for mouseenter/leave events for rows in the table - // to highlight the corresponding anomaly mark in the focus chart. - const tableRecordMousenterListener = function (record) { - highlightFocusChartAnomaly(record); - }; - - const tableRecordMouseleaveListener = function (record) { - unhighlightFocusChartAnomaly(record); - }; - - mlAnomaliesTableService.anomalyRecordMouseenter.watch(tableRecordMousenterListener); - mlAnomaliesTableService.anomalyRecordMouseleave.watch(tableRecordMouseleaveListener); - element.on('$destroy', () => { - mlAnomaliesTableService.anomalyRecordMouseenter.unwatch(tableRecordMousenterListener); - mlAnomaliesTableService.anomalyRecordMouseleave.unwatch(tableRecordMouseleaveListener); resizeChecker.destroy(); scope.$destroy(); }); - function render() { - // Clear any existing elements from the visualization, - // then build the svg elements for the bubble chart. - const chartElement = d3.select(element.get(0)); - chartElement.selectAll('*').remove(); - - if (scope.contextChartData === undefined) { - return; - } - - // Set the size of the components according to the width of the parent container at render time. - svgWidth = Math.max(angular.element('.results-container').width(), 0); - - const svg = chartElement.append('svg') - .attr('width', svgWidth) - .attr('height', svgHeight); - - let contextDataMin; - let contextDataMax; - if (scope.modelPlotEnabled === true || - (scope.contextForecastData !== undefined && scope.contextForecastData.length > 0)) { - const combinedData = scope.contextForecastData === undefined ? - scope.contextChartData : scope.contextChartData.concat(scope.contextForecastData); - - contextDataMin = d3.min(combinedData, d => Math.min(d.value, d.lower)); - contextDataMax = d3.max(combinedData, d => Math.max(d.value, d.upper)); - - } else { - contextDataMin = d3.min(scope.contextChartData, d => d.value); - contextDataMax = d3.max(scope.contextChartData, d => d.value); - } - - - // Set the size of the left margin according to the width of the largest y axis tick label. - // The min / max of the aggregated context chart data may be less than the min / max of the - // data which is displayed in the focus chart which is likely to be plotted at a lower - // aggregation interval. Therefore ceil the min / max with the higher absolute value to allow - // for extra space for chart labels which may have higher values than the context data - // e.g. aggregated max may be 9500, whereas focus plot max may be 11234. - const ceiledMax = contextDataMax > 0 ? - Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMax)))) : contextDataMax; - - const flooredMin = contextDataMin >= 0 ? - contextDataMin : -1 * Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMin)))); - - // Temporarily set the domain of the focus y axis to the min / max of the full context chart - // data range so that we can measure the maximum tick label width on temporary text elements. - focusYScale.domain([flooredMin, ceiledMax]); - - let maxYAxisLabelWidth = 0; - const tempLabelText = svg.append('g') - .attr('class', 'temp-axis-label tick'); - tempLabelText.selectAll('text.temp.axis').data(focusYScale.ticks()) - .enter() - .append('text') - .text((d) => { - if (fieldFormat !== undefined) { - return fieldFormat.convert(d, 'text'); - } else { - return focusYScale.tickFormat()(d); - } - }) - .each(function () { - maxYAxisLabelWidth = Math.max(this.getBBox().width + focusYAxis.tickPadding(), maxYAxisLabelWidth); - }) - .remove(); - d3.select('.temp-axis-label').remove(); - - margin.left = (Math.max(maxYAxisLabelWidth, 40)); - vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); - focusXScale.range([0, vizWidth]); - focusYAxis.innerTickSize(-vizWidth); - - const focus = svg.append('g') - .attr('class', 'focus-chart') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - const context = svg.append('g') - .attr('class', 'context-chart') - .attr('transform', 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')'); - - // Draw each of the component elements. - createFocusChart(focus, vizWidth, focusHeight); - drawContextElements(context, vizWidth, contextChartHeight, swimlaneHeight); - } - - function drawContextChartSelection() { - if (scope.contextChartData === undefined) { - return; - } - - // Make appropriate selection in the context chart to trigger loading of the focus chart. - let focusLoadFrom; - let focusLoadTo; - const contextXMin = contextXScale.domain()[0].getTime(); - const contextXMax = contextXScale.domain()[1].getTime(); - - let combinedData = scope.contextChartData; - if (scope.contextForecastData !== undefined) { - combinedData = combinedData.concat(scope.contextForecastData); - } - - if (scope.zoomFrom) { - focusLoadFrom = scope.zoomFrom.getTime(); - } else { - focusLoadFrom = _.reduce(combinedData, (memo, point) => - Math.min(memo, point.date.getTime()), new Date(2099, 12, 31).getTime()); - } - focusLoadFrom = Math.max(focusLoadFrom, contextXMin); - - if (scope.zoomTo) { - focusLoadTo = scope.zoomTo.getTime(); - } else { - focusLoadTo = _.reduce(combinedData, (memo, point) => Math.max(memo, point.date.getTime()), 0); - } - focusLoadTo = Math.min(focusLoadTo, contextXMax); - - if ((focusLoadFrom !== contextXMin) || (focusLoadTo !== contextXMax)) { - setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); - } else { - // Don't set the brush if the selection is the full context chart domain. - setBrushVisibility(false); - const selectedBounds = contextXScale.domain(); - scope.selectedBounds = { min: moment(new Date(selectedBounds[0])), max: moment(selectedBounds[1]) }; - scope.$root.$broadcast('contextChartSelected', { from: selectedBounds[0], to: selectedBounds[1] }); - } - } - - function createFocusChart(fcsGroup, fcsWidth, fcsHeight) { - // Split out creation of the focus chart from the rendering, - // as we want to re-render the paths and points when the zoom area changes. - - // Add a group at the top to display info on the chart aggregation interval - // and links to set the brush span to 1h, 1d, 1w etc. - const zoomGroup = fcsGroup.append('g') - .attr('class', 'focus-zoom'); - zoomGroup.append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', fcsWidth) - .attr('height', focusZoomPanelHeight) - .attr('class', 'chart-border'); - createZoomInfoElements(zoomGroup, fcsWidth); - - // Add border round plot area. - fcsGroup.append('rect') - .attr('x', 0) - .attr('y', focusZoomPanelHeight) - .attr('width', fcsWidth) - .attr('height', focusChartHeight) - .attr('class', 'chart-border'); - - // Add background for x axis. - const xAxisBg = fcsGroup.append('g') - .attr('class', 'x-axis-background'); - xAxisBg.append('rect') - .attr('x', 0) - .attr('y', fcsHeight) - .attr('width', fcsWidth) - .attr('height', chartSpacing); - xAxisBg.append('line') - .attr('x1', 0) - .attr('y1', fcsHeight) - .attr('x2', 0) - .attr('y2', fcsHeight + chartSpacing); - xAxisBg.append('line') - .attr('x1', fcsWidth) - .attr('y1', fcsHeight) - .attr('x2', fcsWidth) - .attr('y2', fcsHeight + chartSpacing); - xAxisBg.append('line') - .attr('x1', 0) - .attr('y1', fcsHeight + chartSpacing) - .attr('x2', fcsWidth) - .attr('y2', fcsHeight + chartSpacing); - - - const axes = fcsGroup.append('g'); - axes.append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + fcsHeight + ')'); - axes.append('g') - .attr('class', 'y axis'); - - // Create the elements for the metric value line and model bounds area. - fcsGroup.append('path') - .attr('class', 'area bounds'); - fcsGroup.append('path') - .attr('class', 'values-line'); - fcsGroup.append('g') - .attr('class', 'focus-chart-markers'); - - - // Create the path elements for the forecast value line and bounds area. - if (scope.contextForecastData) { - fcsGroup.append('path') - .attr('class', 'area forecast'); - fcsGroup.append('path') - .attr('class', 'values-line forecast'); - fcsGroup.append('g') - .attr('class', 'focus-chart-markers forecast'); - } - - - fcsGroup.append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', fcsWidth) - .attr('height', fcsHeight + 24) - .attr('class', 'chart-border chart-border-highlight'); - } - - function renderFocusChart() { - if (scope.focusChartData === undefined) { - return; - } - - const data = scope.focusChartData; - - const focusChart = d3.select('.focus-chart'); - - // Update the plot interval labels. - const focusAggInt = scope.focusAggregationInterval.expression; - const bucketSpan = scope.selectedJob.analysis_config.bucket_span; - angular.element('.zoom-aggregation-interval').text( - `(aggregation interval: ${focusAggInt}, bucket span: ${bucketSpan})`); - - // Render the axes. - - // Calculate the x axis domain. - // Elasticsearch aggregation returns points at start of bucket, - // so set the x-axis min to the start of the first aggregation interval, - // and the x-axis max to the end of the last aggregation interval. - const bounds = scope.selectedBounds; - const aggMs = scope.focusAggregationInterval.asMilliseconds(); - const earliest = moment(Math.floor((bounds.min.valueOf()) / aggMs) * aggMs); - const latest = moment(Math.ceil((bounds.max.valueOf()) / aggMs) * aggMs); - focusXScale.domain([earliest.toDate(), latest.toDate()]); - - // Calculate the y-axis domain. - if (scope.focusChartData.length > 0 || - (scope.focusForecastData !== undefined && scope.focusForecastData.length > 0)) { - if (fieldFormat !== undefined) { - focusYAxis.tickFormat(d => fieldFormat.convert(d, 'text')); - } else { - // Use default tick formatter. - focusYAxis.tickFormat(null); - } - - // Calculate the min/max of the metric data and the forecast data. - let yMin = 0; - let yMax = 0; - - let combinedData = data; - if (scope.focusForecastData !== undefined && scope.focusForecastData.length > 0) { - combinedData = data.concat(scope.focusForecastData); - } - - yMin = d3.min(combinedData, (d) => { - return d.lower !== undefined ? Math.min(d.value, d.lower) : d.value; - }); - yMax = d3.max(combinedData, (d) => { - return d.upper !== undefined ? Math.max(d.value, d.upper) : d.value; - }); - - if (yMax === yMin) { - if ( - contextYScale.domain()[0] !== contextYScale.domain()[1] && - yMin >= contextYScale.domain()[0] && yMax <= contextYScale.domain()[1] - ) { - // Set the focus chart limits to be the same as the context chart. - yMin = contextYScale.domain()[0]; - yMax = contextYScale.domain()[1]; - } else { - yMin -= (yMin * 0.05); - yMax += (yMax * 0.05); - } - } - - focusYScale.domain([yMin, yMax]); - - } else { - // Display 10 unlabelled ticks. - focusYScale.domain([0, 10]); - focusYAxis.tickFormat(''); - } - - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval('auto'); - timeBuckets.setBounds(bounds); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - focusChart.select('.x.axis') - .call(focusXAxis.ticks(numTicksForDateFormat(vizWidth), xAxisTickFormat) - .tickFormat((d) => { - return moment(d).format(xAxisTickFormat); - })); - focusChart.select('.y.axis') - .call(focusYAxis); - - filterAxisLabels(focusChart.select('.x.axis'), vizWidth); - - // Render the bounds area and values line. - if (scope.modelPlotEnabled === true) { - focusChart.select('.area.bounds') - .attr('d', focusBoundedArea(data)) - .classed('hidden', !scope.showModelBounds); - } - - focusChart.select('.values-line') - .attr('d', focusValuesLine(data)); - drawLineChartDots(data, focusChart, focusValuesLine); - - // Render circle markers for the points. - // These are used for displaying tooltips on mouseover. - // Don't render dots where value=null (data gaps) or for multi-bucket anomalies. - const dots = d3.select('.focus-chart-markers').selectAll('.metric-value') - .data(data.filter(d => (d.value !== null && !showMultiBucketAnomalyMarker(d)))); - - // Remove dots that are no longer needed i.e. if number of chart points has decreased. - dots.exit().remove(); - // Create any new dots that are needed i.e. if number of chart points has increased. - dots.enter().append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - .on('mouseover', function (d) { - showFocusChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all dots to new positions. - dots.attr('cx', (d) => { return focusXScale(d.date); }) - .attr('cy', (d) => { return focusYScale(d.value); }) - .attr('class', (d) => { - let markerClass = 'metric-value'; - if (_.has(d, 'anomalyScore')) { - markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore)}`; - } - return markerClass; - }); - - - // Render cross symbols for any multi-bucket anomalies. - const multiBucketMarkers = d3.select('.focus-chart-markers').selectAll('.multi-bucket') - .data(data.filter(d => (d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true))); - - // Remove multi-bucket markers that are no longer needed. - multiBucketMarkers.exit().remove(); - - // Add any new markers that are needed i.e. if number of multi-bucket points has increased. - multiBucketMarkers.enter().append('path') - .attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross')) - .on('mouseover', function (d) { - showFocusChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all markers to new positions. - multiBucketMarkers.attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) - .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)}`); - - - // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = d3.select('.focus-chart-markers').selectAll('.scheduled-event-marker') - .data(data.filter(d => d.scheduledEvents !== undefined)); - - // Remove markers that are no longer needed i.e. if number of chart points has decreased. - scheduledEventMarkers.exit().remove(); - - // Create any new markers that are needed i.e. if number of chart points has increased. - scheduledEventMarkers.enter().append('rect') - .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) - .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) - .attr('class', 'scheduled-event-marker') - .attr('rx', 1) - .attr('ry', 1); - - // Update all markers to new positions. - scheduledEventMarkers.attr('x', (d) => focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) - .attr('y', (d) => focusYScale(d.value) - 3); - - // Plot any forecast data in scope. - if (scope.focusForecastData !== undefined) { - focusChart.select('.area.forecast') - .attr('d', focusBoundedArea(scope.focusForecastData)) - .classed('hidden', !scope.showForecast); - focusChart.select('.values-line.forecast') - .attr('d', focusValuesLine(scope.focusForecastData)) - .classed('hidden', !scope.showForecast); - - const forecastDots = d3.select('.focus-chart-markers.forecast').selectAll('.metric-value') - .data(scope.focusForecastData); - - // Remove dots that are no longer needed i.e. if number of forecast points has decreased. - forecastDots.exit().remove(); - // Create any new dots that are needed i.e. if number of forecast points has increased. - forecastDots.enter().append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - .on('mouseover', function (d) { - showFocusChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all dots to new positions. - forecastDots.attr('cx', (d) => { return focusXScale(d.date); }) - .attr('cy', (d) => { return focusYScale(d.value); }) - .attr('class', 'metric-value') - .classed('hidden', !scope.showForecast); - } - - } - - function createZoomInfoElements(zoomGroup, fcsWidth) { - // Create zoom duration links applicable for the current time span. - // Don't add links for any durations which would give a brush extent less than 10px. - const bounds = timefilter.getActiveBounds(); - const boundsSecs = bounds.max.unix() - bounds.min.unix(); - const minSecs = (10 / vizWidth) * boundsSecs; - - let xPos = 10; - const zoomLabel = zoomGroup.append('text') - .attr('x', xPos) - .attr('y', 17) - .attr('class', 'zoom-info-text') - .text('Zoom:'); - - const zoomOptions = [{ durationMs: scope.autoZoomDuration, label: 'auto' }]; - _.each(ZOOM_INTERVAL_OPTIONS, (option) => { - if (option.duration.asSeconds() > minSecs && - option.duration.asSeconds() < boundsSecs) { - zoomOptions.push({ durationMs: option.duration.asMilliseconds(), label: option.label }); - } - }); - xPos += (zoomLabel.node().getBBox().width + 4); - - _.each(zoomOptions, (option) => { - const text = zoomGroup.append('a') - .attr('data-ms', option.durationMs) - .attr('href', '') - .append('text') - .attr('x', xPos) - .attr('y', 17) - .attr('class', 'zoom-info-text') - .text(option.label); - - xPos += (text.node().getBBox().width + 4); - }); - - zoomGroup.append('text') - .attr('x', (xPos + 6)) - .attr('y', 17) - .attr('class', 'zoom-info-text zoom-aggregation-interval') - .text('(aggregation interval: , bucket span: )'); - - if (scope.modelPlotEnabled === false) { - const modelPlotLabel = zoomGroup.append('text') - .attr('x', 300) - .attr('y', 17) - .attr('class', 'zoom-info-text') - .text('Model bounds are not available'); - - modelPlotLabel.attr('x', (fcsWidth - (modelPlotLabel.node().getBBox().width + 10))); - } - - $('.focus-zoom a').click(function (e) { - e.preventDefault(); - setZoomInterval($(this).attr('data-ms')); - }); - } - - function drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { - const data = scope.contextChartData; - - contextXScale = d3.time.scale().range([0, cxtWidth]) - .domain(calculateContextXAxisDomain()); - - const combinedData = scope.contextForecastData === undefined ? data : data.concat(scope.contextForecastData); - const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; - _.each(combinedData, (item) => { - valuesRange.min = Math.min(item.value, valuesRange.min); - valuesRange.max = Math.max(item.value, valuesRange.max); - }); - let dataMin = valuesRange.min; - let dataMax = valuesRange.max; - const chartLimits = { min: dataMin, max: dataMax }; - - if (scope.modelPlotEnabled === true || - (scope.contextForecastData !== undefined && scope.contextForecastData.length > 0)) { - const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; - _.each(combinedData, (item) => { - boundsRange.min = Math.min(item.lower, boundsRange.min); - boundsRange.max = Math.max(item.upper, boundsRange.max); - }); - dataMin = Math.min(dataMin, boundsRange.min); - dataMax = Math.max(dataMax, boundsRange.max); - - // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. - if ((valuesRange.max - valuesRange.min) < 0.5 * (dataMax - dataMin)) { - if (valuesRange.min > dataMin) { - chartLimits.min = valuesRange.min - (0.5 * (valuesRange.max - valuesRange.min)); - } - - if (valuesRange.max < dataMax) { - chartLimits.max = valuesRange.max + (0.5 * (valuesRange.max - valuesRange.min)); - } - } - } - - contextYScale = d3.scale.linear().range([cxtChartHeight, contextChartLineTopMargin]) - .domain([chartLimits.min, chartLimits.max]); - - const borders = cxtGroup.append('g') - .attr('class', 'axis'); - - // Add borders left and right. - borders.append('line') - .attr('x1', 0) - .attr('y1', 0) - .attr('x2', 0) - .attr('y2', cxtChartHeight + swlHeight); - borders.append('line') - .attr('x1', cxtWidth) - .attr('y1', 0) - .attr('x2', cxtWidth) - .attr('y2', cxtChartHeight + swlHeight); - - // Add x axis. - const bounds = timefilter.getActiveBounds(); - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval('auto'); - timeBuckets.setBounds(bounds); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - const xAxis = d3.svg.axis().scale(contextXScale) - .orient('top') - .innerTickSize(-cxtChartHeight) - .outerTickSize(0) - .tickPadding(0) - .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) - .tickFormat((d) => { - return moment(d).format(xAxisTickFormat); - }); - - cxtGroup.datum(data); - - const contextBoundsArea = d3.svg.area() - .x((d) => { return contextXScale(d.date); }) - .y0((d) => { return contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); }) - .y1((d) => { return contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); }) - .defined(d => (d.lower !== null && d.upper !== null)); - - if (scope.modelPlotEnabled === true) { - cxtGroup.append('path') - .datum(data) - .attr('class', 'area context') - .attr('d', contextBoundsArea); - } - - const contextValuesLine = d3.svg.line() - .x((d) => { return contextXScale(d.date); }) - .y((d) => { return contextYScale(d.value); }) - .defined(d => d.value !== null); - - cxtGroup.append('path') - .datum(data) - .attr('class', 'values-line') - .attr('d', contextValuesLine); - drawLineChartDots(data, cxtGroup, contextValuesLine, 1); - - // Create the path elements for the forecast value line and bounds area. - if (scope.contextForecastData !== undefined) { - cxtGroup.append('path') - .datum(scope.contextForecastData) - .attr('class', 'area forecast') - .attr('d', contextBoundsArea); - cxtGroup.append('path') - .datum(scope.contextForecastData) - .attr('class', 'values-line forecast') - .attr('d', contextValuesLine); - } - - // Create and draw the anomaly swimlane. - const swimlane = cxtGroup.append('g') - .attr('class', 'swimlane') - .attr('transform', 'translate(0,' + cxtChartHeight + ')'); - - drawSwimlane(swimlane, cxtWidth, swlHeight); - - // Draw a mask over the sections of the context chart and swimlane - // which fall outside of the zoom brush selection area. - mask = new ContextChartMask(cxtGroup, scope.contextChartData, scope.modelPlotEnabled, swlHeight) - .x(contextXScale) - .y(contextYScale); - - // Draw the x axis on top of the mask so that the labels are visible. - cxtGroup.append('g') - .attr('class', 'x axis context-chart-axis') - .call(xAxis); - - // Move the x axis labels up so that they are inside the contact chart area. - cxtGroup.selectAll('.x.context-chart-axis text') - .attr('dy', (cxtChartHeight - 5)); - - filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); - - drawContextBrush(cxtGroup); - } - - function drawContextBrush(contextGroup) { - // Create the brush for zooming in to the focus area of interest. - brush.x(contextXScale) - .on('brush', brushing) - .on('brushend', brushed); - - contextGroup.append('g') - .attr('class', 'x brush') - .call(brush) - .selectAll('rect') - .attr('y', -1) - .attr('height', contextChartHeight + swimlaneHeight + 1); - - // move the left and right resize areas over to - // be under the handles - contextGroup.selectAll('.w rect') - .attr('x', -10) - .attr('width', 10); - - contextGroup.selectAll('.e rect') - .attr('x', 0) - .attr('width', 10); - - const topBorder = contextGroup.append('rect') - .attr('class', 'top-border') - .attr('y', -2) - .attr('height', contextChartLineTopMargin); - - // Draw the brush handles using SVG foreignObject elements. - // Note these are not supported on IE11 and below, so will not appear in IE. - const leftHandle = contextGroup.append('foreignObject') - .attr('width', 10) - .attr('height', 90) - .attr('class', 'brush-handle') - .html('
'); - const rightHandle = contextGroup.append('foreignObject') - .attr('width', 10) - .attr('height', 90) - .attr('class', 'brush-handle') - .html('
'); - - setBrushVisibility(!brush.empty()); - - function showBrush(show) { - if (show === true) { - const brushExtent = brush.extent(); - mask.reveal(brushExtent); - leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); - rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); - - topBorder.attr('x', contextXScale(brushExtent[0]) + 1); - topBorder.attr('width', contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2); - } - - setBrushVisibility(show); - } - - function brushing() { - const isEmpty = brush.empty(); - showBrush(!isEmpty); - } - - function brushed() { - const isEmpty = brush.empty(); - showBrush(!isEmpty); - - const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); - const selectionMin = selectedBounds[0].getTime(); - const selectionMax = selectedBounds[1].getTime(); - - // Set the color of the swimlane cells according to whether they are inside the selection. - contextGroup.selectAll('.swimlane-cell') - .style('fill', (d) => { - const cellMs = d.date.getTime(); - if (cellMs < selectionMin || cellMs > selectionMax) { - return anomalyGrayScale(d.score); - } else { - return anomalyColorScale(d.score); - } - }); - - scope.selectedBounds = { min: moment(selectionMin), max: moment(selectionMax) }; - scope.$root.$broadcast('contextChartSelected', { from: selectedBounds[0], to: selectedBounds[1] }); - } - } - - function setBrushVisibility(show) { - if (mask !== undefined) { - const visibility = show ? 'visible' : 'hidden'; - mask.style('visibility', visibility); - - d3.selectAll('.brush').style('visibility', visibility); - - const brushHandles = d3.selectAll('.brush-handle-inner'); - brushHandles.style('visibility', visibility); - - const topBorder = d3.selectAll('.top-border'); - topBorder.style('visibility', visibility); - - const border = d3.selectAll('.chart-border-highlight'); - border.style('visibility', visibility); - } - } - - function drawSwimlane(swlGroup, swlWidth, swlHeight) { - const data = scope.swimlaneData; - - // Calculate the x axis domain. - // Elasticsearch aggregation returns points at start of bucket, so set the - // x-axis min to the start of the aggregation interval. - // Need to use the min(earliest) and max(earliest) of the context chart - // aggregation to align the axes of the chart and swimlane elements. - const xAxisDomain = calculateContextXAxisDomain(); - const x = d3.time.scale().range([0, swlWidth]) - .domain(xAxisDomain); - - const y = d3.scale.linear().range([swlHeight, 0]) - .domain([0, swlHeight]); - - const xAxis = d3.svg.axis() - .scale(x) - .orient('bottom') - .innerTickSize(-swlHeight) - .outerTickSize(0); - - const yAxis = d3.svg.axis() - .scale(y) - .orient('left') - .tickValues(y.domain()) - .innerTickSize(-swlWidth) - .outerTickSize(0); - - const axes = swlGroup.append('g'); - - axes.append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + (swlHeight) + ')') - .call(xAxis); - - axes.append('g') - .attr('class', 'y axis') - .call(yAxis); - - const earliest = xAxisDomain[0].getTime(); - const latest = xAxisDomain[1].getTime(); - const swimlaneAggMs = scope.contextAggregationInterval.asMilliseconds(); - let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); - if (cellWidth < 1) { - cellWidth = 1; - } - - const cells = swlGroup.append('g') - .attr('class', 'swimlane-cells') - .selectAll('cells') - .data(data); - - cells.enter().append('rect') - .attr('x', (d) => { return x(d.date); }) - .attr('y', 0) - .attr('rx', 0) - .attr('ry', 0) - .attr('class', (d) => { return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden';}) - .attr('width', cellWidth) - .attr('height', swlHeight) - .style('fill', (d) => { return anomalyColorScale(d.score);}); - - } - - function calculateContextXAxisDomain() { - // Calculates the x axis domain for the context elements. - // Elasticsearch aggregation returns points at start of bucket, - // so set the x-axis min to the start of the first aggregation interval, - // and the x-axis max to the end of the last aggregation interval. - // Context chart and swimlane use the same aggregation interval. - const bounds = timefilter.getActiveBounds(); - let earliest = bounds.min.valueOf(); - - if (scope.swimlaneData !== undefined && scope.swimlaneData.length > 0) { - // Adjust the earliest back to the time of the first swimlane point - // if this is before the time filter minimum. - earliest = Math.min(_.first(scope.swimlaneData).date.getTime(), bounds.min.valueOf()); - } - - const contextAggMs = scope.contextAggregationInterval.asMilliseconds(); - const earliestMs = Math.floor(earliest / contextAggMs) * contextAggMs; - const latestMs = Math.ceil((bounds.max.valueOf()) / contextAggMs) * contextAggMs; - - return [new Date(earliestMs), new Date(latestMs)]; - } - - // Sets the extent of the brush on the context chart to the - // supplied from and to Date objects. - function setContextBrushExtent(from, to, fireEvent) { - brush.extent([from, to]); - brush(d3.select('.brush')); - if (fireEvent) { - brush.event(d3.select('.brush')); - } - } - - function setZoomInterval(ms) { - const bounds = timefilter.getActiveBounds(); - const minBoundsMs = bounds.min.valueOf(); - const maxBoundsMs = bounds.max.valueOf(); - - // Attempt to retain the same zoom end time. - // If not, go back to the bounds start and add on the required millis. - const millis = +ms; - let to = scope.zoomTo.getTime(); - let from = to - millis; - if (from < minBoundsMs) { - from = minBoundsMs; - to = Math.min(minBoundsMs + millis, maxBoundsMs); - } - - setContextBrushExtent(new Date(from), new Date(to), true); - } - - function showFocusChartTooltip(marker, circle) { - // Show the time and metric values in the tooltip. - // Uses date, value, upper, lower and anomalyScore (optional) marker properties. - const formattedDate = moment(marker.date).format('MMMM Do YYYY, HH:mm'); - let contents = formattedDate + '

'; - - if (_.has(marker, 'anomalyScore')) { - const score = parseInt(marker.anomalyScore); - const displayScore = (score > 0 ? score : '< 1'); - contents += `anomaly score: ${displayScore}
`; - - if (showMultiBucketAnomalyTooltip(marker) === true) { - contents += `multi-bucket impact: ${getMultiBucketImpactLabel(marker.multiBucketImpact)}
`; - } - - if (scope.modelPlotEnabled === false) { - // Show actual/typical when available except for rare detectors. - // Rare detectors always have 1 as actual and the probability as typical. - // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && marker.function !== 'rare') { - // Display the record actual in preference to the chart value, which may be - // different depending on the aggregation interval of the chart. - contents += `actual: ${formatValue(marker.actual, marker.function, fieldFormat)}`; - contents += `
typical: ${formatValue(marker.typical, marker.function, fieldFormat)}`; - } else { - contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; - if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { - const numberOfCauses = marker.numberOfCauses; - const byFieldName = mlEscape(marker.byFieldName); - if (numberOfCauses < 10) { - // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. - contents += `
${numberOfCauses} unusual ${byFieldName} values`; - } else { - // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. - contents += `
${numberOfCauses}+ unusual ${byFieldName} values`; - } - } - } - } else { - contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; - contents += `
upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`; - contents += `
lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`; - } - } else { - // TODO - need better formatting for small decimals. - if (_.get(marker, 'isForecast', false) === true) { - contents += `prediction: ${formatValue(marker.value, marker.function, fieldFormat)}`; - } else { - contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`; - } - - if (scope.modelPlotEnabled === true) { - contents += `
upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`; - contents += `
lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`; - } - } - - if (_.has(marker, 'scheduledEvents')) { - contents += `

Scheduled events:
${marker.scheduledEvents.map(mlEscape).join('
')}`; - } - - mlChartTooltipService.show(contents, circle, { - x: LINE_CHART_ANOMALY_RADIUS * 2, - y: 0 - }); - } - - function highlightFocusChartAnomaly(record) { - // Highlights the anomaly marker in the focus chart corresponding to the specified record. - - // Find the anomaly marker which corresponds to the time of the anomaly record. - // Depending on the way the chart is aggregated, there may not be - // a point at exactly the same time as the record being highlighted. - const anomalyTime = record.source.timestamp; - const markerToSelect = findChartPointForAnomalyTime(scope.focusChartData, anomalyTime); - - // Render an additional highlighted anomaly marker on the focus chart. - // TODO - plot anomaly markers for cases where there is an anomaly due - // to the absence of data and model plot is enabled. - if (markerToSelect !== undefined) { - const selectedMarker = d3.select('.focus-chart-markers') - .selectAll('.focus-chart-highlighted-marker') - .data([markerToSelect]); - if (showMultiBucketAnomalyMarker(markerToSelect) === true) { - selectedMarker.enter().append('path') - .attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross')) - .attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) - .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)} highlighted`); - } else { - selectedMarker.enter().append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - .attr('cx', d => focusXScale(d.date)) - .attr('cy', d => focusYScale(d.value)) - .attr('class', d => `anomaly-marker metric-value ${getSeverityWithLow(d.anomalyScore)} highlighted`); - } - - // Display the chart tooltip for this marker. - // Note the values of the record and marker may differ depending on the levels of aggregation. - const anomalyMarker = $('.focus-chart-markers .anomaly-marker.highlighted'); - if (anomalyMarker.length) { - showFocusChartTooltip(markerToSelect, anomalyMarker[0]); - } - } - } - - function unhighlightFocusChartAnomaly() { - d3.select('.focus-chart-markers').selectAll('.anomaly-marker.highlighted').remove(); - mlChartTooltipService.hide(); - } - - } return { diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html index 6db9a87d735ca47..f0d243f8f916683 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html @@ -115,6 +115,7 @@ zoom-to="zoomTo" auto-zoom-duration="autoZoomDuration"> +
diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js index 9e6d92caca15749..4d9ffc3349dd40c 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js @@ -15,7 +15,7 @@ import _ from 'lodash'; import { parseInterval } from 'ui/utils/parse_interval'; -import { isTimeSeriesViewJob } from 'plugins/ml/../common/util/job_utils'; +import { isTimeSeriesViewJob } from '../../common/util/job_utils'; // create new job objects based on standard job config objects // new job objects just contain job id, bucket span in seconds and a selected flag. diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js index 06d5f8081659354..34f4daddba6510f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js @@ -197,6 +197,7 @@ export class DataRecognizer { groups, indexPatternName, query, + useDedicatedIndex, startDatafeed, start, end, @@ -234,6 +235,11 @@ export class DataRecognizer { // update groups list for each job moduleConfig.jobs.forEach(job => job.config.groups = groups); } + + // Set the results_index_name property for each job if useDedicatedIndex is true + if (useDedicatedIndex === true) { + moduleConfig.jobs.forEach(job => job.config.results_index_name = job.id); + } saveResults.jobs = await this.saveJobs(moduleConfig.jobs); } @@ -361,7 +367,7 @@ export class DataRecognizer { // save the savedObjects if they do not exist already async saveKibanaObjects(objectExistResults) { - let results = []; + let results = { saved_objects: [] }; const filteredSavedObjects = objectExistResults.filter(o => o.exists === false).map(o => o.savedObject); if (filteredSavedObjects.length) { results = await this.savedObjectsClient.bulkCreate(filteredSavedObjects); diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/dashboard/ml_auditbeat_docker_audit_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/dashboard/ml_auditbeat_docker_audit_events.json new file mode 100644 index 000000000000000..26715920b0641e7 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/dashboard/ml_auditbeat_docker_audit_events.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Docker: Audit Events", + "description": "All events occurring within docker containers", + "panelsJSON": "[{\"gridData\":{\"x\":0,\"y\":0,\"w\":13,\"h\":13,\"i\":\"1\"},\"version\":\"6.4.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_docker_container_count\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":13,\"y\":0,\"w\":35,\"h\":13,\"i\":\"2\"},\"version\":\"6.4.0\",\"panelIndex\":\"2\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_docker_container_images\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":0,\"y\":13,\"w\":48,\"h\":13,\"i\":\"3\"},\"version\":\"6.4.0\",\"panelIndex\":\"3\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_docker_container_event_volume\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":24,\"y\":26,\"w\":24,\"h\":15,\"i\":\"4\"},\"version\":\"6.4.0\",\"panelIndex\":\"4\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_docker_processes\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":0,\"y\":26,\"w\":24,\"h\":15,\"i\":\"5\"},\"version\":\"6.4.0\",\"panelIndex\":\"5\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_docker_process_presence\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":24,\"y\":41,\"w\":24,\"h\":15,\"i\":\"6\"},\"version\":\"6.4.0\",\"panelIndex\":\"6\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_docker_commands\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":0,\"y\":41,\"w\":24,\"h\":15,\"i\":\"7\"},\"version\":\"6.4.0\",\"panelIndex\":\"7\",\"type\":\"search\",\"id\":\"ml_auditbeat_docker_events\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/search/ml_auditbeat_docker_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/search/ml_auditbeat_docker_events.json new file mode 100644 index 000000000000000..e11519c4b3d8de8 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/search/ml_auditbeat_docker_events.json @@ -0,0 +1,16 @@ +{ + "title": "ML Auditbeat Docker: Docker Events", + "description": "Audit Events Correlated with Docker Metadata", + "hits": 0, + "columns": [ + "_source" + ], + "sort": [ + "@timestamp", + "desc" + ], + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"INDEX_PATTERN_ID\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"index\":\"INDEX_PATTERN_ID\",\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"exists\",\"key\":\"docker.container.id\",\"value\":\"exists\"},\"exists\":{\"field\":\"docker.container.id\"},\"$state\":{\"store\":\"appState\"}}]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_commands.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_commands.json new file mode 100644 index 000000000000000..d83c5c0b424308e --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_commands.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Docker: Commands", + "visState": "{\"title\":\"ML Auditbeat Docker: Commands\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"process.title\",\"size\":100,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "description": "", + "savedSearchId": "ml_auditbeat_docker_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_count.json new file mode 100644 index 000000000000000..bd4806fbf2d47c6 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_count.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Docker: Container Count", + "visState": "{\"title\":\"ML Auditbeat Docker: Container Count\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"docker.container.id\"}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_docker_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_event_volume.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_event_volume.json new file mode 100644 index 000000000000000..a3dc7b816789bf6 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_event_volume.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Docker: Container Event Volume", + "visState": "{\"title\":\"ML Auditbeat Docker: Container Event Volume\",\"type\":\"line\",\"params\":{\"type\":\"line\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"line\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"docker.container.id\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_docker_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_images.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_images.json new file mode 100644 index 000000000000000..749117c2ed08052 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_container_images.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Docker: Container Images", + "visState": "{\"title\":\"ML Auditbeat Docker: Container Images\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"docker.container.image\",\"size\":100,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "description": "", + "savedSearchId": "ml_auditbeat_docker_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_process_presence.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_process_presence.json new file mode 100644 index 000000000000000..ed90d87f11913b2 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_process_presence.json @@ -0,0 +1,12 @@ + +{ + "title": "ML Auditbeat Docker: Process Presence", + "visState": "{\"title\":\"ML Auditbeat Docker: Process Presence\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Unique\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Unique\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"process.exe\",\"customLabel\":\"Unique\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"process.exe\",\"size\":20,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"docker.container.name\",\"size\":1,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"row\":true}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_docker_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_processes.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_processes.json new file mode 100644 index 000000000000000..eb03494149921e3 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/kibana/visualization/ml_auditbeat_docker_processes.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Docker: Processes", + "visState": "{\"title\":\"ML Auditbeat Docker: Processes\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"process.exe\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"docker.container.name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"row\":true}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_docker_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/logo.json new file mode 100644 index 000000000000000..8f5e61d1b765c95 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/logo.json @@ -0,0 +1,5 @@ +{ + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAADFBMVEUAAAAAAAABf3X////ZaOWRAAAAAXRSTlMAQObYZgAAAAFiS0dEAxEMTPIAAAAfSURBVBjTYwgNDXVqBBIMcEYAAwNTAwMD60hkYIQGAIQRIolX2EV0AAAAAElFTkSuQmCC", + "height": 32, + "width": 32 +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/manifest.json new file mode 100644 index 000000000000000..7f6806739499f0c --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/manifest.json @@ -0,0 +1,86 @@ +{ + "id": "auditbeat_process_docker", + "title": "Auditbeat Docker processes", + "description": "Detect unusual processes on Docker containers", + "type": "Auditbeat data", + "logoFile": "logo.json", + "defaultIndexPattern": "auditbeat-*", + "query": { + "bool": { + "must": [ + { + "exists": { + "field": "auditd.summary" + } + }, + { + "exists": { + "field": "docker.container.id" + } + } + ] + } + }, + "jobs": [ + { + "id": "docker_high_count_events", + "file": "docker_high_count_events.json" + }, + { + "id": "docker_suspicious_process_activity", + "file": "docker_suspicious_process_activity.json" + } + ], + "datafeeds": [ + { + "id": "datafeed-docker_high_count_events", + "file": "datafeed_docker_high_count_events.json", + "job_id": "docker_high_count_events" + }, + { + "id": "datafeed-docker_suspicious_process_activity", + "file": "datafeed_docker_suspicious_process_activity.json", + "job_id": "docker_suspicious_process_activity" + } + ], + "kibana": { + "dashboard": [ + { + "id": "ml_auditbeat_docker_audit_events", + "file": "ml_auditbeat_docker_audit_events.json" + } + ], + "search": [ + { + "id": "ml_auditbeat_docker_events", + "file": "ml_auditbeat_docker_events.json" + } + ], + "visualization": [ + { + "id": "ml_auditbeat_docker_commands", + "file": "ml_auditbeat_docker_commands.json" + }, + { + "id": "ml_auditbeat_docker_container_count", + "file": "ml_auditbeat_docker_container_count.json" + }, + { + "id": "ml_auditbeat_docker_container_event_volume", + "file": "ml_auditbeat_docker_container_event_volume.json" + }, + { + "id": "ml_auditbeat_docker_container_images", + "file": "ml_auditbeat_docker_container_images.json" + }, + { + "id": "ml_auditbeat_docker_processes", + "file": "ml_auditbeat_docker_processes.json" + }, + { + "id": "ml_auditbeat_docker_process_presence", + "file": "ml_auditbeat_docker_process_presence.json" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/datafeed_docker_high_count_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/datafeed_docker_high_count_events.json new file mode 100644 index 000000000000000..84dea03a07712b9 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/datafeed_docker_high_count_events.json @@ -0,0 +1,27 @@ +{ + "job_id": "JOB_ID", + "indexes": [ + "INDEX_PATTERN_NAME" + ], + "types": [], + "query": { + "bool": { + "must": [ + { + "match": { + "event.type": "syscall" + } + }, + { + "exists": { + "field":"docker.container.id" + } + } + ] + } + }, + "scroll_size": 1000, + "chunking_config": { + "mode": "auto" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/datafeed_docker_suspicious_process_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/datafeed_docker_suspicious_process_activity.json new file mode 100644 index 000000000000000..84dea03a07712b9 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/datafeed_docker_suspicious_process_activity.json @@ -0,0 +1,27 @@ +{ + "job_id": "JOB_ID", + "indexes": [ + "INDEX_PATTERN_NAME" + ], + "types": [], + "query": { + "bool": { + "must": [ + { + "match": { + "event.type": "syscall" + } + }, + { + "exists": { + "field":"docker.container.id" + } + } + ] + } + }, + "scroll_size": 1000, + "chunking_config": { + "mode": "auto" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/docker_high_count_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/docker_high_count_events.json new file mode 100644 index 000000000000000..c6423265cb0e527 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/docker_high_count_events.json @@ -0,0 +1,35 @@ +{ + "job_type": "anomaly_detector", + "description": "Auditbeat: Detect Unusual Increases in Docker Process Volume", + "groups": ["auditbeat"], + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "detector_description": "high_count partitionfield=\"docker.container.id\"", + "function": "high_count", + "partition_field_name": "docker.container.id" + } + ], + "influencers": [ + "process.exe" + ] + }, + "analysis_limits": { + "model_memory_limit": "256mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "custom_settings": { + "custom_urls": [ + { + "url_name": "Docker Events", + "time_range": "1h", + "url_value": "kibana#/dashboard/ml_auditbeat_docker_audit_events?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(filters:!(),query:(language:lucene,query:'docker.container.id:\"$docker.container.id$\"'))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/docker_suspicious_process_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/docker_suspicious_process_activity.json new file mode 100644 index 000000000000000..0e58fb96219dff7 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker/ml/docker_suspicious_process_activity.json @@ -0,0 +1,35 @@ +{ + "job_type": "anomaly_detector", + "description": "Auditbeat: Detect Rare Process Executions in Docker Containers", + "groups": ["auditbeat"], + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "detector_description": "rare by 'process.exe'", + "function": "rare", + "by_field_name": "process.exe" + } + ], + "influencers": [ + "process.exe", + "docker.container.id" + ] + }, + "analysis_limits": { + "model_memory_limit": "256mb" + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "custom_settings": { + "custom_urls": [ + { + "url_name": "Docker Events", + "time_range": "1h", + "url_value": "kibana#/dashboard/ml_auditbeat_docker_audit_events?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(filters:!(),query:(language:lucene,query:'docker.container.id:\"$docker.container.id$\" AND process.exe:\"$process.exe$\"'))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/dashboard/ml_auditbeat_hosts_audit_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/dashboard/ml_auditbeat_hosts_audit_events.json new file mode 100644 index 000000000000000..e5be851d5089254 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/dashboard/ml_auditbeat_hosts_audit_events.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Audit Events", + "description": "All events occuring directly on host machines", + "panelsJSON": "[{\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":12,\"i\":\"1\"},\"version\":\"6.4.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_hosts_event_volume\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":24,\"y\":12,\"w\":24,\"h\":15,\"i\":\"2\"},\"version\":\"6.4.0\",\"panelIndex\":\"2\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_hosts_kernel_actions\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":0,\"y\":12,\"w\":24,\"h\":15,\"i\":\"3\"},\"version\":\"6.4.0\",\"panelIndex\":\"3\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_hosts_kernel_action_presence\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":24,\"y\":27,\"w\":24,\"h\":15,\"i\":\"4\"},\"version\":\"6.4.0\",\"panelIndex\":\"4\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_hosts_processes\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":0,\"y\":27,\"w\":24,\"h\":15,\"i\":\"5\"},\"version\":\"6.4.0\",\"panelIndex\":\"5\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_hosts_process_presence\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":0,\"y\":42,\"w\":24,\"h\":15,\"i\":\"6\"},\"version\":\"6.4.0\",\"panelIndex\":\"6\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_hosts_command_line\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":24,\"y\":42,\"w\":24,\"h\":15,\"i\":\"7\"},\"version\":\"6.4.0\",\"panelIndex\":\"7\",\"type\":\"visualization\",\"id\":\"ml_auditbeat_hosts_exe_thing\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":0,\"y\":57,\"w\":24,\"h\":15,\"i\":\"8\"},\"version\":\"6.4.0\",\"panelIndex\":\"8\",\"type\":\"search\",\"id\":\"ml_auditbeat_hosts_events\",\"embeddableConfig\":{}},{\"gridData\":{\"x\":24,\"y\":57,\"w\":24,\"h\":15,\"i\":\"9\"},\"version\":\"6.4.0\",\"panelIndex\":\"9\",\"type\":\"search\",\"id\":\"ml_auditbeat_all_events\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/search/ml_auditbeat_all_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/search/ml_auditbeat_all_events.json new file mode 100644 index 000000000000000..f46420eec239ba0 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/search/ml_auditbeat_all_events.json @@ -0,0 +1,16 @@ +{ + "title": "ML Auditbeat: All Events", + "description": "All Audit Events Captured By Auditbeat", + "hits": 0, + "columns": [ + "_source" + ], + "sort": [ + "@timestamp", + "desc" + ], + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"INDEX_PATTERN_ID\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/search/ml_auditbeat_hosts_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/search/ml_auditbeat_hosts_events.json new file mode 100644 index 000000000000000..3c0db9408e51955 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/search/ml_auditbeat_hosts_events.json @@ -0,0 +1,16 @@ +{ + "title": "ML Auditbeat Hosts: Host Events", + "description": "Audit Events occurring directly on host machines", + "hits": 0, + "columns": [ + "_source" + ], + "sort": [ + "@timestamp", + "desc" + ], + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"INDEX_PATTERN_ID\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"index\":\"INDEX_PATTERN_ID\",\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"exists\",\"key\":\"docker.container.id\",\"value\":\"exists\"},\"exists\":{\"field\":\"docker.container.id\"},\"$state\":{\"store\":\"appState\"}}]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_command_line.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_command_line.json new file mode 100644 index 000000000000000..58e5e60d9472c3d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_command_line.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Command Line", + "visState": "{\"title\":\"ML Auditbeat Hosts: Command Line\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"process.title\",\"size\":100,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "description": "", + "savedSearchId": "ml_auditbeat_hosts_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_event_volume.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_event_volume.json new file mode 100644 index 000000000000000..83ee6bc38897ed1 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_event_volume.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Event Volume", + "visState": "{\"title\":\"ML Auditbeat Hosts: Event Volume\",\"type\":\"line\",\"params\":{\"type\":\"line\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"line\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"beat.name\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_hosts_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_exe_thing.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_exe_thing.json new file mode 100644 index 000000000000000..f7603880ccfec5d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_exe_thing.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Exe Thing", + "visState": "{\"title\":\"ML Auditbeat Hosts: Exe Thing\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"process.exe\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"auditd.summary.object.primary\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_hosts_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_kernel_action_presence.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_kernel_action_presence.json new file mode 100644 index 000000000000000..941f6e0e57cdd6b --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_kernel_action_presence.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Kernel Action Presence", + "visState": "{\"title\":\"ML Auditbeat Hosts: Kernel Action Presence\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Unique count of event.action\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Unique count of event.action\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"event.action\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"event.action\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_hosts_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_kernel_actions.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_kernel_actions.json new file mode 100644 index 000000000000000..911c25101dbd498 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_kernel_actions.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Kernel Actions", + "visState": "{\"title\":\"ML Auditbeat Hosts: Kernel Actions\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"event.action\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_hosts_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_process_presence.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_process_presence.json new file mode 100644 index 000000000000000..94a83135cf2cdf5 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_process_presence.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Process Presence", + "visState": "{\"title\":\"ML Auditbeat Hosts: Process Presence\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Unique count of process.exe\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Unique count of process.exe\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"process.exe\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_hosts_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_processes.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_processes.json new file mode 100644 index 000000000000000..bc0c6da5b8618da --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/kibana/visualization/ml_auditbeat_hosts_processes.json @@ -0,0 +1,11 @@ +{ + "title": "ML Auditbeat Hosts: Processes", + "visState": "{\"title\":\"ML Auditbeat Hosts: Processes\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"process.exe\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "ml_auditbeat_hosts_events", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/logo.json new file mode 100644 index 000000000000000..8f5e61d1b765c95 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/logo.json @@ -0,0 +1,5 @@ +{ + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAADFBMVEUAAAAAAAABf3X////ZaOWRAAAAAXRSTlMAQObYZgAAAAFiS0dEAxEMTPIAAAAfSURBVBjTYwgNDXVqBBIMcEYAAwNTAwMD60hkYIQGAIQRIolX2EV0AAAAAElFTkSuQmCC", + "height": 32, + "width": 32 +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/manifest.json new file mode 100644 index 000000000000000..f60b8e514ea638a --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/manifest.json @@ -0,0 +1,96 @@ +{ + "id": "auditbeat_process_hosts", + "title": "Auditbeat host processes", + "description": "Detect unusual processes on hosts", + "type": "Auditbeat data", + "logoFile": "logo.json", + "defaultIndexPattern": "auditbeat-*", + "query": { + "bool": { + "must": [ + { + "exists": { + "field": "auditd.summary" + } + } + ], + "must_not": [ + { + "exists": { + "field": "docker.container.id" + } + } + ] + } + }, + "jobs": [ + { + "id": "hosts_high_count_events", + "file": "hosts_high_count_events.json" + }, + { + "id": "hosts_suspicious_process_activity", + "file": "hosts_suspicious_process_activity.json" + } + ], + "datafeeds": [ + { + "id": "datafeed-hosts_high_count_events", + "file": "datafeed_hosts_high_count_events.json", + "job_id": "hosts_high_count_events" + }, + { + "id": "datafeed-hosts_suspicious_process_activity", + "file": "datafeed_hosts_suspicious_process_activity.json", + "job_id": "hosts_suspicious_process_activity" + } + ], + "kibana": { + "dashboard": [ + { + "id": "ml_auditbeat_hosts_audit_events", + "file": "ml_auditbeat_hosts_audit_events.json" + } + ], + "search": [ + { + "id": "ml_auditbeat_hosts_events", + "file": "ml_auditbeat_hosts_events.json" + }, + { + "id": "ml_auditbeat_all_events", + "file": "ml_auditbeat_all_events.json" + } + ], + "visualization": [ + { + "id": "ml_auditbeat_hosts_command_line", + "file": "ml_auditbeat_hosts_command_line.json" + }, + { + "id": "ml_auditbeat_hosts_event_volume", + "file": "ml_auditbeat_hosts_event_volume.json" + }, + { + "id": "ml_auditbeat_hosts_exe_thing", + "file": "ml_auditbeat_hosts_exe_thing.json" + }, + { + "id": "ml_auditbeat_hosts_kernel_action_presence", + "file": "ml_auditbeat_hosts_kernel_action_presence.json" + }, + { + "id": "ml_auditbeat_hosts_kernel_actions", + "file": "ml_auditbeat_hosts_kernel_actions.json" + }, + { + "id": "ml_auditbeat_hosts_process_presence", + "file": "ml_auditbeat_hosts_process_presence.json" + }, + { + "id": "ml_auditbeat_hosts_processes", + "file": "ml_auditbeat_hosts_processes.json" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/datafeed_hosts_high_count_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/datafeed_hosts_high_count_events.json new file mode 100644 index 000000000000000..a9fab9b33f1a783 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/datafeed_hosts_high_count_events.json @@ -0,0 +1,29 @@ +{ + "job_id": "JOB_ID", + "indexes": [ + "INDEX_PATTERN_NAME" + ], + "types": [], + "query":{ + "bool": { + "must":[ + { + "match": { + "event.type": "syscall" + } + } + ], + "must_not": [ + { + "exists": { + "field": "docker.container.id" + } + } + ] + } + }, + "scroll_size": 1000, + "chunking_config": { + "mode": "auto" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/datafeed_hosts_suspicious_process_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/datafeed_hosts_suspicious_process_activity.json new file mode 100644 index 000000000000000..f760e7f2ab08823 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/datafeed_hosts_suspicious_process_activity.json @@ -0,0 +1,29 @@ +{ + "job_id": "JOB_ID", + "indexes": [ + "INDEX_PATTERN_NAME" + ], + "types": [], + "query":{ + "bool": { + "must":[ + { + "match": { + "event.type": "syscall" + } + } + ], + "must_not": [ + { + "exists": { + "field": "docker.container.id" + } + } + ] + } +}, + "scroll_size": 1000, + "chunking_config": { + "mode": "auto" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/hosts_high_count_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/hosts_high_count_events.json new file mode 100644 index 000000000000000..6ec1f172e751d2b --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/hosts_high_count_events.json @@ -0,0 +1,36 @@ +{ + "job_type": "anomaly_detector", + "description": "Auditbeat Hosts: Detect Unusual Increases in Host Process Volume", + "groups": ["auditbeat"], + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "detector_description": "high_count partitionfield=\"beat.name\"", + "function": "high_count", + "partition_field_name": "beat.name" + } + ], + "influencers": [ + "beat.name", + "process.exe" + ] + }, + "analysis_limits": { + "model_memory_limit": "256mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "custom_settings": { + "custom_urls": [ + { + "url_name": "Host Events", + "time_range": "1h", + "url_value": "kibana#/dashboard/ml_auditbeat_hosts_audit_events?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(filters:!(),query:(language:lucene,query:'beat.name:\"$beat.name$\"'))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/hosts_suspicious_process_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/hosts_suspicious_process_activity.json new file mode 100644 index 000000000000000..1464165393cc2ab --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts/ml/hosts_suspicious_process_activity.json @@ -0,0 +1,35 @@ +{ + "job_type": "anomaly_detector", + "description": "Auditbeat Hosts: Detect Rare Process Executions on Hosts", + "groups": ["auditbeat"], + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "detector_description": "rare by 'process.exe'", + "function": "rare", + "by_field_name": "process.exe" + } + ], + "influencers": [ + "process.exe", + "beat.name" + ] + }, + "analysis_limits": { + "model_memory_limit": "256mb" + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "custom_settings": { + "custom_urls": [ + { + "url_name": "Host Events", + "time_range": "1h", + "url_value": "kibana#/dashboard/ml_auditbeat_hosts_audit_events?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(filters:!(),query:(language:lucene,query:'beat.name:\"$beat.name$\" AND process.exe:\"$process.exe$\"'))" + } + ] + } +} diff --git a/x-pack/plugins/ml/server/routes/job_validation.js b/x-pack/plugins/ml/server/routes/job_validation.js index 2096169e232d274..73e3a8685bb8f95 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.js +++ b/x-pack/plugins/ml/server/routes/job_validation.js @@ -85,9 +85,7 @@ export function jobValidationRoutes(server, commonRouteConfig) { const callWithRequest = callWithRequestFactory(server, request); return validateCardinality(callWithRequest, request.payload) .then(reply) - .catch((resp) => { - reply(wrapError(resp)); - }); + .catch(resp => wrapError(resp)); }, config: { ...commonRouteConfig diff --git a/x-pack/plugins/ml/server/routes/modules.js b/x-pack/plugins/ml/server/routes/modules.js index 94c6e5919e4d9e0..0bac2adc69a0662 100644 --- a/x-pack/plugins/ml/server/routes/modules.js +++ b/x-pack/plugins/ml/server/routes/modules.js @@ -28,6 +28,7 @@ function saveModuleItems( groups, indexPatternName, query, + useDedicatedIndex, startDatafeed, start, end, @@ -40,6 +41,7 @@ function saveModuleItems( groups, indexPatternName, query, + useDedicatedIndex, startDatafeed, start, end, @@ -88,6 +90,7 @@ export function dataRecognizer(server, commonRouteConfig) { groups, indexPatternName, query, + useDedicatedIndex, startDatafeed, start, end @@ -100,6 +103,7 @@ export function dataRecognizer(server, commonRouteConfig) { groups, indexPatternName, query, + useDedicatedIndex, startDatafeed, start, end, diff --git a/x-pack/plugins/monitoring/common/constants.js b/x-pack/plugins/monitoring/common/constants.js index 7bc8164130adffe..852afe74c461e10 100644 --- a/x-pack/plugins/monitoring/common/constants.js +++ b/x-pack/plugins/monitoring/common/constants.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + /** * Helper string to add as a tag in every logging call */ @@ -129,10 +131,12 @@ export const LOGSTASH = { } }; -export const DEFAULT_NO_DATA_MESSAGE = 'There are no records that match your query. Try changing the time range selection.'; -export const DEFAULT_NO_DATA_MESSAGE_WITH_FILTER = ( - 'There are no records that match your query with the filter [{{FILTER}}]. Try changing the filter or the time range selection.' -); +export const DEFAULT_NO_DATA_MESSAGE = i18n.translate('xpack.monitoring.defaultNoDataMessage', { + defaultMessage: 'There are no records that match your query. Try changing the time range selection.' }); +export const DEFAULT_NO_DATA_MESSAGE_WITH_FILTER = i18n.translate('xpack.monitoring.defaultNoDataWithFilterMessage', { + defaultMessage: + 'There are no records that match your query with the filter [{filter}]. Try changing the filter or the time range selection.', + values: { filter: '{{FILTER}}' } }); export const TABLE_ACTION_UPDATE_FILTER = 'UPDATE_FILTER'; export const TABLE_ACTION_RESET_PAGING = 'RESET_PAGING'; diff --git a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js index 29ff9de73a10891..2b3f348893402b6 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js +++ b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js @@ -29,24 +29,41 @@ import { capitalize } from 'lodash'; * @param {Number} severity The number representing the severity. Higher is "worse". * @return {Object} An object containing details about the severity. */ + +import { i18n } from '@kbn/i18n'; + export function mapSeverity(severity) { const floor = Math.floor(severity / 1000); let mapped; switch (floor) { case 0: - mapped = { value: 'low', color: 'warning', iconType: 'dot' }; + mapped = { + value: i18n.translate('xpack.monitoring.alerts.lowSeverityName', { defaultMessage: 'low' }), + color: 'warning', + iconType: 'dot' + }; break; case 1: - mapped = { value: 'medium', color: 'warning', iconType: 'dot' }; + mapped = { + value: i18n.translate('xpack.monitoring.alerts.mediumSeverityName', { defaultMessage: 'medium' }), + color: 'warning', + iconType: 'dot' + }; break; default: // severity >= 2000 - mapped = { value: 'high', color: 'danger', iconType: 'dot' }; + mapped = { + value: i18n.translate('xpack.monitoring.alerts.highSeverityName', { defaultMessage: 'high' }), + color: 'danger', + iconType: 'dot' + }; break; } return { - title: `${capitalize(mapped.value)} severity alert`, + title: i18n.translate('xpack.monitoring.alerts.severityTitle', + { defaultMessage: '{severity} severity alert', values: { severity: capitalize(mapped.value) } } + ), ...mapped }; -} \ No newline at end of file +} diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js index e3397e01ad91386..318b44304bccbac 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js @@ -8,12 +8,13 @@ import React from 'react'; import { Tooltip } from 'plugins/monitoring/components/tooltip'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { EuiHealth } from '@elastic/eui'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; const HIGH_SEVERITY = 2000; const MEDIUM_SEVERITY = 1000; const LOW_SEVERITY = 0; -export function AlertsIndicator({ alerts }) { +function AlertsIndicatorUi({ alerts, intl }) { if (alerts && alerts.count > 0) { const severity = (() => { if (alerts.high > 0) { return HIGH_SEVERITY; } @@ -24,29 +25,49 @@ export function AlertsIndicator({ alerts }) { const tooltipText = (() => { switch (severity) { case HIGH_SEVERITY: - return 'There are some critical cluster issues that require your immediate attention!'; + return intl.formatMessage({ + id: 'xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip', + defaultMessage: 'There are some critical cluster issues that require your immediate attention!' }); case MEDIUM_SEVERITY: - return 'There are some issues that might have impact on your cluster.'; + return intl.formatMessage({ + id: 'xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip', + defaultMessage: 'There are some issues that might have impact on your cluster.' }); default: // might never show - return 'There are some low-severity cluster issues'; + return intl.formatMessage({ + id: 'xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip', + defaultMessage: 'There are some low-severity cluster issues' }); } })(); return ( - Alerts + ); } return ( - + - Clear + ); } + +export const AlertsIndicator = injectI18n(AlertsIndicatorUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index 6f04cdfcbc0862f..de0318ea3f502cd 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -10,6 +10,7 @@ import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; import { formatDateTimeLocal } from '../../../../common/formatting'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -22,7 +23,7 @@ import { EuiCallOut, } from '@elastic/eui'; -export function AlertsPanel({ alerts, changeUrl }) { +function AlertsPanelUi({ alerts, changeUrl, intl }) { const goToAlerts = () => changeUrl('/alerts'); if (!alerts || !alerts.length) { @@ -35,8 +36,11 @@ export function AlertsPanel({ alerts, changeUrl }) { const severityIcon = mapSeverity(item.metadata.severity); if (item.resolved_timestamp) { - severityIcon.title = - `${severityIcon.title} (resolved ${formatTimestampToDuration(item.resolved_timestamp, CALCULATE_DURATION_SINCE)} ago)`; + severityIcon.title = intl.formatMessage({ + id: 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', + defaultMessage: '{severityIconTitle} (resolved {time} ago)' }, + { severityIconTitle: severityIcon.title, time: formatTimestampToDuration(item.resolved_timestamp, CALCULATE_DURATION_SINCE) + }); severityIcon.color = "success"; severityIcon.iconType = "check"; } @@ -60,11 +64,14 @@ export function AlertsPanel({ alerts, changeUrl }) {

- Last checked { - formatDateTimeLocal(item.update_timestamp) - } (triggered { - formatTimestampToDuration(item.timestamp, CALCULATE_DURATION_SINCE) - } ago) +

@@ -80,13 +87,19 @@ export function AlertsPanel({ alerts, changeUrl }) {

- Top cluster alerts +

- View all alerts + @@ -96,3 +109,5 @@ export function AlertsPanel({ alerts, changeUrl }) {
); } + +export const AlertsPanel = injectI18n(AlertsPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index a5726ab46666c15..d031f6a57e02b2d 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -9,6 +9,7 @@ import moment from 'moment'; import { get } from 'lodash'; import { formatMetric } from 'plugins/monitoring/lib/format_number'; import { ClusterItemContainer, BytesPercentageUsage } from './helpers'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { EuiFlexGrid, @@ -22,8 +23,9 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { formatTimestampToDuration } from '../../../../common'; +import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; -export function ApmPanel(props) { +function ApmPanelUi(props) { if (!get(props, 'apms.total', 0) > 0) { return null; } @@ -32,7 +34,11 @@ export function ApmPanel(props) { const goToInstances = () => props.changeUrl('apm/instances'); return ( - + @@ -40,22 +46,40 @@ export function ApmPanel(props) {

- Overview +

- Processed Events + + + {formatMetric(props.totalEvents, '0.[0]a')} - Last Event + + + - {formatTimestampToDuration(+moment(props.timeOfLastEvent), 'since') + ' ago'} +
@@ -66,16 +90,28 @@ export function ApmPanel(props) {

- APM Servers: {props.apms.total} + {props.apms.total}) }} + />

- Memory Usage + + + @@ -86,3 +122,5 @@ export function ApmPanel(props) {
); } + +export const ApmPanel = injectI18n(ApmPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index 8a0b2e279e9fa99..1909698d529636c 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -19,8 +19,9 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { ClusterItemContainer } from './helpers'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -export function BeatsPanel(props) { +function BeatsPanelUi(props) { if (!get(props, 'beats.total', 0) > 0) { return null; } @@ -46,7 +47,11 @@ export function BeatsPanel(props) { }); return ( - + @@ -54,20 +59,34 @@ export function BeatsPanel(props) {

- Overview +

- Total Events + + + {formatMetric(props.totalEvents, '0.[0]a')} - Bytes Sent + + + {formatMetric(props.bytesSent, 'byte')} @@ -80,10 +99,17 @@ export function BeatsPanel(props) {

- Beats: {props.beats.total} + {props.beats.total}) }} + />

@@ -97,3 +123,5 @@ export function BeatsPanel(props) {
); } + +export const BeatsPanel = injectI18n(BeatsPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index f3c6f6e8d31735c..a87e5b7232cc6d6 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -20,6 +20,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { LicenseText } from './license_text'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; const calculateShards = shards => { const total = get(shards, 'total', 0); @@ -39,7 +40,7 @@ const calculateShards = shards => { }; }; -export function ElasticsearchPanel(props) { +function ElasticsearchPanelUi(props) { const clusterStats = props.cluster_stats || {}; const nodes = clusterStats.nodes; @@ -59,7 +60,12 @@ export function ElasticsearchPanel(props) { // if license doesn't support ML, then `ml === null` if (props.ml) { return [ - Jobs, + + + , { props.ml.jobs } ]; } @@ -69,7 +75,13 @@ export function ElasticsearchPanel(props) { const licenseText = ; return ( - + @@ -78,20 +90,35 @@ export function ElasticsearchPanel(props) {

- Overview +

- Version + + + - { props.version || 'N/A' } + { props.version || props.intl.formatMessage({ + id: 'xpack.monitoring.cluster.overview.esPanel.versionNotAvailableDescription', defaultMessage: 'N/A' }) } - Uptime + + + { formatNumber(get(nodes, 'jvm.max_uptime_in_millis'), 'time_since') } @@ -108,20 +135,35 @@ export function ElasticsearchPanel(props) { data-test-subj="esNumberOfNodes" onClick={goToNodes} > - Nodes: { formatNumber(get(nodes, 'count.total'), 'int_commas') } + - Disk Available + + + - JVM Heap + + + - Indices: { formatNumber(get(indices, 'count'), 'int_commas') } + - Documents + { formatNumber(get(indices, 'docs.count'), 'int_commas') } - Disk Usage + { formatNumber(get(indices, 'store.size_in_bytes'), 'byte') } - Primary Shards + { primaries } - Replica Shards + { replicas } @@ -182,3 +243,5 @@ export function ElasticsearchPanel(props) {
); } + +export const ElasticsearchPanel = injectI18n(ElasticsearchPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js index 88c0e1b91940f79..b4446177f0bb8d3 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js @@ -16,6 +16,7 @@ import { EuiHealth, EuiText, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; export function HealthStatusIndicator(props) { @@ -29,7 +30,11 @@ export function HealthStatusIndicator(props) { return ( - Health is {props.status} + ); } diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index 924064a5e2a2a8b..e7e6e08717b2874 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -19,8 +19,9 @@ import { EuiDescriptionListDescription, EuiHorizontalRule, } from '@elastic/eui'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -export function KibanaPanel(props) { +function KibanaPanelUi(props) { if (!props.count) { return null; } @@ -33,7 +34,13 @@ export function KibanaPanel(props) { const goToInstances = () => props.changeUrl('kibana/instances'); return ( - + @@ -41,22 +48,40 @@ export function KibanaPanel(props) {

- Overview +

- Requests + + + { props.requests_total } - Max. Response Time + + + - { props.response_time_max } ms +
@@ -68,19 +93,36 @@ export function KibanaPanel(props) { - Instances: { props.count } + { props.count }) }} + /> - Connections + + + { formatNumber(props.concurrent_connections, 'int_commas') } - Memory Usage + + + @@ -91,3 +133,5 @@ export function KibanaPanel(props) {
); } + +export const KibanaPanel = injectI18n(KibanaPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js b/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js index 17cd262a078b9e7..0dc646aced96017 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js @@ -4,21 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React from 'react'; import moment from 'moment-timezone'; import { capitalize } from 'lodash'; import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; const formatDateLocal = input => moment.tz(input, moment.tz.guess()).format('LL'); -const WillExpireOn = ({ expiryDate }) => { - if (expiryDate === undefined) { - return null; - } - - return will expire on {formatDateLocal(expiryDate)}; -}; - export function LicenseText({ license, showLicenseExpiration }) { if (!showLicenseExpiration) { return null; @@ -26,7 +19,20 @@ export function LicenseText({ license, showLicenseExpiration }) { return ( - {capitalize(license.type)} license + + ) + }} + /> ); } diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 5394bcc8507bbc2..c26f7124d7f6d25 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -21,8 +21,9 @@ import { EuiDescriptionListDescription, EuiHorizontalRule, } from '@elastic/eui'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -export function LogstashPanel(props) { +function LogstashPanelUi(props) { if (!props.node_count) { return null; } @@ -32,7 +33,12 @@ export function LogstashPanel(props) { const goToPipelines = () => props.changeUrl('logstash/pipelines'); return ( - + @@ -40,19 +46,33 @@ export function LogstashPanel(props) {

- Overview +

- Events Received + + + { formatNumber(props.events_in_total, '0.[0]a') } - Events Emitted + + + { formatNumber(props.events_out_total, '0.[0]a') } @@ -67,19 +87,38 @@ export function LogstashPanel(props) { - Nodes: { props.node_count } + { props.node_count }) }} + /> - Uptime + + + { formatNumber(props.max_uptime, 'time_since') } - JVM Heap + + + @@ -94,24 +133,44 @@ export function LogstashPanel(props) { - Pipelines: { props.pipeline_count } + { props.pipeline_count }) }} + /> - With Memory Queues + + + { props.queue_types[LOGSTASH.QUEUE_TYPES.MEMORY] } - With Persistent Queues + + + { props.queue_types[LOGSTASH.QUEUE_TYPES.PERSISTED] }
@@ -120,3 +179,5 @@ export function LogstashPanel(props) {
); } + +export const LogstashPanel = injectI18n(LogstashPanelUi); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js index f05496d9adeb779..3ddbe31bcd37351 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js @@ -18,12 +18,13 @@ import { } from '@elastic/eui'; import './ccr.css'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; function toSeconds(ms) { return Math.floor(ms / 1000) + 's'; } -export class Ccr extends Component { +class CcrUI extends Component { constructor(props) { super(props); this.state = { @@ -32,6 +33,7 @@ export class Ccr extends Component { } toggleShards(index, shards) { + const { intl } = this.props; const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; @@ -54,7 +56,10 @@ export class Ccr extends Component { columns={[ { field: 'shardId', - name: 'Shard', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.shardColumnTitle', + defaultMessage: 'Shard' + }), render: shardId => { return ( @@ -68,7 +73,10 @@ export class Ccr extends Component { }, { field: 'syncLagOps', - name: 'Sync Lag (ops)', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.syncLagOpsColumnTitle', + defaultMessage: 'Sync Lag (ops)' + }), render: (syncLagOps, data) => ( {syncLagOps} @@ -78,9 +86,25 @@ export class Ccr extends Component { type="iInCircle" content={( - Leader lag: {data.syncLagOpsLeader} + + +
- Follower lag: {data.syncLagOpsFollower} + + +
)} position="right" @@ -90,16 +114,25 @@ export class Ccr extends Component { }, { field: 'syncLagTime', - name: 'Last fetch time', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.lastFetchTimeColumnTitle', + defaultMessage: 'Last fetch time' + }), render: syncLagTime => {toSeconds(syncLagTime)} }, { field: 'opsSynced', - name: 'Ops synced' + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.opsSyncedColumnTitle', + defaultMessage: 'Ops synced' + }), }, { field: 'error', - name: 'Error', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.shardsTable.errorColumnTitle', + defaultMessage: 'Error' + }), render: error => ( {error} @@ -116,7 +149,7 @@ export class Ccr extends Component { } renderTable() { - const { data } = this.props; + const { data, intl } = this.props; const items = data; let pagination = { @@ -141,7 +174,10 @@ export class Ccr extends Component { columns={[ { field: 'index', - name: 'Index', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.indexColumnTitle', + defaultMessage: 'Index' + }), sortable: true, render: (index, { shards }) => { const expanded = !!this.state.itemIdToExpandedRowMap[index]; @@ -157,28 +193,43 @@ export class Ccr extends Component { { field: 'follows', sortable: true, - name: 'Follows' + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.followsColumnTitle', + defaultMessage: 'Follows' + }), }, { field: 'syncLagOps', sortable: true, - name: 'Sync Lag (ops)', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.syncLagOpsColumnTitle', + defaultMessage: 'Sync Lag (ops)' + }), }, { field: 'syncLagTime', sortable: true, - name: 'Last fetch time', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.lastFetchTimeColumnTitle', + defaultMessage: 'Last fetch time' + }), render: syncLagTime => {toSeconds(syncLagTime)} }, { field: 'opsSynced', sortable: true, - name: 'Ops synced' + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.opsSyncedColumnTitle', + defaultMessage: 'Ops synced' + }), }, { field: 'error', sortable: true, - name: 'Error', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccr.ccrListingTable.errorColumnTitle', + defaultMessage: 'Error' + }), render: error => ( {error} @@ -209,3 +260,5 @@ export class Ccr extends Component { ); } } + +export const Ccr = injectI18n(CcrUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js index 8df42974c663307..838d3fcb5c6c47a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallowWithIntl } from '../../../../../../test_utils/enzyme_helpers'; import { Ccr } from './ccr'; describe('Ccr', () => { @@ -67,7 +67,7 @@ describe('Ccr', () => { } ]; - const component = shallow(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap index 70051f82611c50d..aa5059ed84e16bc 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap @@ -16,7 +16,11 @@ exports[`CcrShard that is renders an exception properly 1`] = ` color="danger" component="span" > - Errors + @@ -63,7 +67,7 @@ exports[`CcrShard that it renders normally 1`] = ` -

- Advanced +

} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js index 552b00dbeb8f3dd..872bfd94ca8fce5 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js @@ -22,8 +22,9 @@ import { import { MonitoringTimeseriesContainer } from '../../chart'; import { Status } from './status'; import { formatDateTimeLocal } from '../../../../common/formatting'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -export class CcrShard extends PureComponent { +class CcrShardUI extends PureComponent { renderCharts() { const { metrics } = this.props; const seriesToShow = [ @@ -49,14 +50,19 @@ export class CcrShard extends PureComponent { } renderErrors() { - const { stat } = this.props; + const { stat, intl } = this.props; if (stat.read_exceptions && stat.read_exceptions.length > 0) { return (

- Errors + + +

@@ -64,11 +70,17 @@ export class CcrShard extends PureComponent { items={stat.read_exceptions} columns={[ { - name: 'Type', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccrShard.errorsTable.typeColumnTitle', + defaultMessage: 'Type' + }), field: 'exception.type' }, { - name: 'Reason', + name: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccrShard.errorsTable.reasonColumnTitle', + defaultMessage: 'Reason' + }), field: 'exception.reason', width: '75%' } @@ -88,7 +100,16 @@ export class CcrShard extends PureComponent { return (

Advanced

} + buttonContent={( + +

+ +

+
+ )} paddingSize="l" > @@ -123,3 +144,5 @@ export class CcrShard extends PureComponent { ); } } + +export const CcrShard = injectI18n(CcrShardUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js index 40a0f32931d3e14..cbd1ce79a383cb1 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallowWithIntl } from '../../../../../../test_utils/enzyme_helpers'; import { CcrShard } from './ccr_shard'; describe('CcrShard', () => { @@ -45,7 +45,7 @@ describe('CcrShard', () => { }; test('that it renders normally', () => { - const component = shallow(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -63,7 +63,7 @@ describe('CcrShard', () => { } }; - const component = shallow(); + const component = shallowWithIntl(); expect(component.find('EuiPanel').get(0)).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js index 969f6fe2fc45e74..89da6c6d4fac3d6 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js @@ -7,8 +7,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { formatMetric } from '../../../lib/format_number'; +import { injectI18n } from '@kbn/i18n/react'; -export function Status({ stat, formattedLeader, oldestStat }) { +function StatusUI({ stat, formattedLeader, oldestStat, intl }) { const { follower_index: followerIndex, shard_id: shardId, @@ -23,27 +24,42 @@ export function Status({ stat, formattedLeader, oldestStat }) { const metrics = [ { - label: 'Follower Index', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccrShard.status.followerIndexLabel', + defaultMessage: 'Follower Index', + }), value: followerIndex, dataTestSubj: 'followerIndex' }, { - label: 'Shard Id', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccrShard.status.shardIdLabel', + defaultMessage: 'Shard Id', + }), value: shardId, dataTestSubj: 'shardId' }, { - label: 'Leader Index', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccrShard.status.leaderIndexLabel', + defaultMessage: 'Leader Index', + }), value: formattedLeader, dataTestSubj: 'leaderIndex' }, { - label: 'Ops Synced', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccrShard.status.opsSyncedLabel', + defaultMessage: 'Ops Synced', + }), value: formatMetric(operationsReceived - oldestOperationsReceived, 'int_commas'), dataTestSubj: 'operationsReceived' }, { - label: 'Failed Fetches', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.ccrShard.status.failedFetchesLabel', + defaultMessage: 'Failed Fetches', + }), value: formatMetric(failedFetches - oldestFailedFetches, 'int_commas'), dataTestSubj: 'failedFetches' }, @@ -56,3 +72,5 @@ export function Status({ stat, formattedLeader, oldestStat }) { /> ); } + +export const Status = injectI18n(StatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js index 9fc2ba4c1e8e34d..a7d007e4d22f1a3 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js @@ -8,8 +8,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { ElasticsearchStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; +import { injectI18n } from '@kbn/i18n/react'; -export function ClusterStatus({ stats }) { +function ClusterStatusUI({ stats, intl }) { const { dataSize, nodesCount, @@ -24,37 +25,58 @@ export function ClusterStatus({ stats }) { const metrics = [ { - label: 'Nodes', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.clusterStatus.nodesLabel', + defaultMessage: 'Nodes', + }), value: nodesCount, dataTestSubj: 'nodesCount' }, { - label: 'Indices', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.clusterStatus.indicesLabel', + defaultMessage: 'Indices', + }), value: indicesCount, dataTestSubj: 'indicesCount' }, { - label: 'Memory', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.clusterStatus.memoryLabel', + defaultMessage: 'Memory', + }), value: formatMetric(memUsed, 'byte') + ' / ' + formatMetric(memMax, 'byte'), dataTestSubj: 'memory' }, { - label: 'Total Shards', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.clusterStatus.totalShardsLabel', + defaultMessage: 'Total Shards', + }), value: totalShards, dataTestSubj: 'totalShards' }, { - label: 'Unassigned Shards', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.clusterStatus.unassignedShardsLabel', + defaultMessage: 'Unassigned Shards', + }), value: unassignedShards, dataTestSubj: 'unassignedShards' }, { - label: 'Documents', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.clusterStatus.documentsLabel', + defaultMessage: 'Documents', + }), value: formatMetric(documentCount, 'int_commas'), dataTestSubj: 'documentCount' }, { - label: 'Data', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.clusterStatus.dataLabel', + defaultMessage: 'Data', + }), value: formatMetric(dataSize, 'byte'), dataTestSubj: 'dataSize' } @@ -73,3 +95,5 @@ export function ClusterStatus({ stats }) { /> ); } + +export const ClusterStatus = injectI18n(ClusterStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js index 2eb378b298085e3..55e65608b81f7f9 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js @@ -8,8 +8,9 @@ import React, { Fragment } from 'react'; import { SummaryStatus } from '../../summary_status'; import { ElasticsearchStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -export function IndexDetailStatus({ stats }) { +function IndexDetailStatusUI({ stats, intl }) { const { dataSize, documents: documentCount, @@ -20,27 +21,42 @@ export function IndexDetailStatus({ stats }) { const metrics = [ { - label: 'Total', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.indexDetailStatus.totalTitle', + defaultMessage: 'Total', + }), value: formatMetric(dataSize.total, '0.0 b'), dataTestSubj: 'dataSize' }, { - label: 'Primaries', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.indexDetailStatus.primariesTitle', + defaultMessage: 'Primaries', + }), value: formatMetric(dataSize.primaries, '0.0 b'), dataTestSubj: 'dataSizePrimaries' }, { - label: 'Documents', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.indexDetailStatus.documentsTitle', + defaultMessage: 'Documents', + }), value: formatMetric(documentCount, '0.[0]a'), dataTestSubj: 'documentCount' }, { - label: 'Total Shards', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.indexDetailStatus.totalShardsTitle', + defaultMessage: 'Total Shards', + }), value: formatMetric(totalShards, 'int_commas'), dataTestSubj: 'totalShards' }, { - label: 'Unassigned Shards', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.indexDetailStatus.unassignedShardsTitle', + defaultMessage: 'Unassigned Shards', + }), value: formatMetric(unassignedShards, 'int_commas'), dataTestSubj: 'unassignedShards' } @@ -48,7 +64,15 @@ export function IndexDetailStatus({ stats }) { const IconComponent = ({ status }) => ( - Health: + + ) + }} + /> ); @@ -61,3 +85,5 @@ export function IndexDetailStatus({ stats }) { /> ); } + +export const IndexDetailStatus = injectI18n(IndexDetailStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js index bd497142ad846c0..ba41ea8fb82d65c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js @@ -15,18 +15,57 @@ import { MonitoringTable } from '../../table'; import { EuiLink } from '@elastic/eui'; import { KuiTableRowCell, KuiTableRow } from '@kbn/ui-framework/components'; import { SystemIndicesCheckbox } from './system_indices_checkbox'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; const filterFields = ['name', 'status']; const columns = [ - { title: 'Name', sortKey: 'name', secondarySortOrder: SORT_ASCENDING }, - { title: 'Status', sortKey: 'status_sort', sortOrder: SORT_DESCENDING }, // default sort: red, then yellow, then green - { title: 'Document Count', sortKey: 'doc_count' }, - { title: 'Data', sortKey: 'data_size' }, - { title: 'Index Rate', sortKey: 'index_rate' }, - { title: 'Search Rate', sortKey: 'search_rate' }, - { title: 'Unassigned Shards', sortKey: 'unassigned_shards' } + { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.nameTitle', { + defaultMessage: 'Name', + }), + sortKey: 'name', + secondarySortOrder: SORT_ASCENDING + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.statusTitle', { + defaultMessage: 'Status', + }), + sortKey: 'status_sort', + sortOrder: SORT_DESCENDING // default sort: red, then yellow, then green + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.documentCountTitle', { + defaultMessage: 'Document Count', + }), + sortKey: 'doc_count' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.dataTitle', { + defaultMessage: 'Data', + }), + sortKey: 'data_size' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.indexRateTitle', { + defaultMessage: 'Index Rate', + }), + sortKey: 'index_rate' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.searchRateTitle', { + defaultMessage: 'Search Rate', + }), + sortKey: 'search_rate' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.indices.unassignedShardsTitle', { + defaultMessage: 'Unassigned Shards', + }), + sortKey: 'unassigned_shards' + } ]; -const IndexRow = ({ status, ...props }) => ( +const IndexRow = injectI18n(({ status, ...props }) => ( ( -
+
  {capitalize(status)}
@@ -58,26 +103,45 @@ const IndexRow = ({ status, ...props }) => ( {formatMetric(get(props, 'unassigned_shards'), '0')} -); +)); const getNoDataMessage = filterText => { + const howToShowSystemIndicesDescription = ( + + ); if (filterText) { return (

- There are no indices that match your selection with the filter [{filterText.trim()}]. - Try changing the filter or the time range selection. +

- If you are looking for system indices (e.g., .kibana), try checking ‘Show system indices’. + {howToShowSystemIndicesDescription}

); } return (
-

There are no indices that match your selections. Try changing the time range selection.

-

If you are looking for system indices (e.g., .kibana), try checking ‘Show system indices’.

+

+ +

+

+ {howToShowSystemIndicesDescription} +

); }; @@ -90,7 +154,7 @@ const renderToolBarSection = ({ showSystemIndices, toggleShowSystemIndices, ...p /> ); -export function ElasticsearchIndices({ clusterStatus, indices, ...props }) { +function ElasticsearchIndicesUI({ clusterStatus, indices, intl, ...props }) { return ( @@ -102,7 +166,10 @@ export function ElasticsearchIndices({ clusterStatus, indices, ...props }) { sortKey={props.sortKey} sortOrder={props.sortOrder} onNewState={props.onNewState} - placeholder="Filter Indices..." + placeholder={intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.indices.monitoringTablePlaceholder', + defaultMessage: 'Filter Indices…', + })} filterFields={filterFields} renderToolBarSections={renderToolBarSection} columns={columns} @@ -115,3 +182,4 @@ export function ElasticsearchIndices({ clusterStatus, indices, ...props }) { ); } +export const ElasticsearchIndices = injectI18n(ElasticsearchIndicesUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/system_indices_checkbox.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/system_indices_checkbox.js index 4620d144f24bcc6..4d341f00b4c9361 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/system_indices_checkbox.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/system_indices_checkbox.js @@ -13,6 +13,7 @@ import { EuiSwitch, } from '@elastic/eui'; import { TABLE_ACTION_RESET_PAGING } from '../../../../common/constants'; +import { FormattedMessage } from '@kbn/i18n/react'; export class SystemIndicesCheckbox extends React.Component { constructor(props) { @@ -31,7 +32,12 @@ export class SystemIndicesCheckbox extends React.Component { + )} onChange={this.toggleShowSystemIndices} checked={this.state.showSystemIndices} /> diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js index 58063a35434bdaf..110cfc9f06e2af4 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js @@ -6,8 +6,9 @@ import React from 'react'; import { StatusIcon } from 'plugins/monitoring/components/status_icon'; +import { injectI18n } from '@kbn/i18n/react'; -export function MachineLearningJobStatusIcon({ status }) { +export function MachineLearningJobStatusIconUI({ status, intl }) { const type = (() => { const statusKey = status.toUpperCase(); @@ -24,6 +25,14 @@ export function MachineLearningJobStatusIcon({ status }) { })(); return ( - + ); } + +export const MachineLearningJobStatusIcon = injectI18n(MachineLearningJobStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js index 5a7ed469eba35cd..73d9abcf59904e8 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js @@ -6,11 +6,20 @@ import React from 'react'; import { StatusIcon } from '../../status_icon'; +import { injectI18n } from '@kbn/i18n/react'; -export function NodeStatusIcon({ isOnline, status }) { +export function NodeStatusIconUI({ isOnline, status, intl }) { const type = isOnline ? StatusIcon.TYPES.GREEN : StatusIcon.TYPES.GRAY; return ( - + ); } + +export const NodeStatusIcon = injectI18n(NodeStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js index 099114baa253c14..a901a36c7325a87 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js @@ -8,8 +8,9 @@ import React, { Fragment } from 'react'; import { SummaryStatus } from '../../summary_status'; import { NodeStatusIcon } from '../node'; import { formatMetric } from '../../../lib/format_number'; +import { injectI18n } from '@kbn/i18n/react'; -export function NodeDetailStatus({ stats }) { +function NodeDetailStatusUI({ stats, intl }) { const { transport_address: transportAddress, usedHeap, @@ -29,37 +30,59 @@ export function NodeDetailStatus({ stats }) { dataTestSubj: 'transportAddress' }, { - label: 'JVM Heap', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.jvmHeapLabel', + defaultMessage: '{javaVirtualMachine} Heap' }, { + javaVirtualMachine: 'JVM' + }), value: formatMetric(usedHeap, '0,0.[00]', '%', { prependSpace: false }), dataTestSubj: 'jvmHeap' }, { - label: 'Free Disk Space', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.freeDiskSpaceLabel', + defaultMessage: 'Free Disk Space', + }), value: formatMetric(freeSpace, '0.0 b'), dataTestSubj: 'freeDiskSpace' }, { - label: 'Documents', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.documentsLabel', + defaultMessage: 'Documents', + }), value: formatMetric(documents, '0.[0]a'), dataTestSubj: 'documentCount' }, { - label: 'Data', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.dataLabel', + defaultMessage: 'Data', + }), value: formatMetric(dataSize, '0.0 b'), dataTestSubj: 'dataSize' }, { - label: 'Indices', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.indicesLabel', + defaultMessage: 'Indices', + }), value: formatMetric(indexCount, 'int_commas'), dataTestSubj: 'indicesCount' }, { - label: 'Shards', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.shardsLabel', + defaultMessage: 'Shards', + }), value: formatMetric(totalShards, 'int_commas'), dataTestSubj: 'shardsCount' }, { - label: 'Type', + label: intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodeDetailStatus.typeLabel', + defaultMessage: 'Type', + }), value: nodeTypeLabel, dataTestSubj: 'nodeType' } @@ -81,3 +104,5 @@ export function NodeDetailStatus({ stats }) { /> ); } + +export const NodeDetailStatus = injectI18n(NodeDetailStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js index 39a7b2e62307a13..bdd453bb976cf4e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render } from 'enzyme'; +import { renderWithIntl } from '../../../../../../../test_utils/enzyme_helpers'; import { MetricCell } from '../cells'; describe('Node Listing Metric Cell', () => { @@ -28,7 +28,7 @@ describe('Node Listing Metric Cell', () => { summary: { minVal: 0, maxVal: 2, lastVal: 0, slope: -1 } } }; - expect(render()).toMatchSnapshot(); + expect(renderWithIntl()).toMatchSnapshot(); }); it('should format a non-percentage metric', () => { @@ -55,11 +55,11 @@ describe('Node Listing Metric Cell', () => { } } }; - expect(render()).toMatchSnapshot(); + expect(renderWithIntl()).toMatchSnapshot(); }); it('should format N/A as the metric for an offline node', () => { const props = { isOnline: false }; - expect(render()).toMatchSnapshot(); + expect(renderWithIntl()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js index a6faa0433351f0b..8fb336a8fc85e3b 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js @@ -8,12 +8,16 @@ import React from 'react'; import { get } from 'lodash'; import { formatMetric } from '../../../lib/format_number'; import { KuiTableRowCell } from '@kbn/ui-framework/components'; +import { FormattedMessage } from '@kbn/i18n/react'; function OfflineCell() { return (
- N/A +
); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 7129f7c09dc8e61..9f05ccf50ed15e9 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -14,25 +14,72 @@ import { MonitoringTable } from '../../table'; import { MetricCell, OfflineCell } from './cells'; import { EuiLink, EuiToolTip } from '@elastic/eui'; import { KuiTableRowCell, KuiTableRow } from '@kbn/ui-framework/components'; +import { i18n } from '@kbn/i18n'; +import { injectI18n } from '@kbn/i18n/react'; const filterFields = ['name']; const getColumns = showCgroupMetricsElasticsearch => { const cols = []; - cols.push({ title: 'Name', sortKey: 'name', sortOrder: SORT_ASCENDING }); - cols.push({ title: 'Status', sortKey: 'isOnline' }); + cols.push({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.nameColumnTitle', { + defaultMessage: 'Name', + }), + sortKey: 'name', + sortOrder: SORT_ASCENDING + }); + cols.push({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumnTitle', { + defaultMessage: 'Status', + }), + sortKey: 'isOnline' + }); + const cpuUsageColumnTitle = i18n.translate('xpack.monitoring.elasticsearch.nodes.cpuUsageColumnTitle', { + defaultMessage: 'CPU Usage', + }); if (showCgroupMetricsElasticsearch) { - cols.push({ title: 'CPU Usage', sortKey: 'node_cgroup_quota' }); cols.push({ - title: 'CPU Throttling', + title: cpuUsageColumnTitle, + sortKey: 'node_cgroup_quota' + }); + cols.push({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.cpuThrottlingColumnTitle', { + defaultMessage: 'CPU Throttling', + }), sortKey: 'node_cgroup_throttled' }); } else { - cols.push({ title: 'CPU Usage', sortKey: 'node_cpu_utilization' }); - cols.push({ title: 'Load Average', sortKey: 'node_load_average' }); + cols.push({ + title: cpuUsageColumnTitle, + sortKey: 'node_cpu_utilization' + }); + cols.push({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.loadAverageColumnTitle', { + defaultMessage: 'Load Average', + }), + sortKey: 'node_load_average' + }); } - cols.push({ title: 'JVM Memory', sortKey: 'node_jvm_mem_percent' }); - cols.push({ title: 'Disk Free Space', sortKey: 'node_free_space' }); - cols.push({ title: 'Shards', sortKey: 'shardCount' }); + cols.push({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.jvmMemoryColumnTitle', { + defaultMessage: '{javaVirtualMachine} Memory', + values: { + javaVirtualMachine: 'JVM' + } + }), + sortKey: 'node_jvm_mem_percent' + }); + cols.push({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.diskFreeSpaceColumnTitle', { + defaultMessage: 'Disk Free Space', + }), + sortKey: 'node_free_space' + }); + cols.push({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.shardsColumnTitle', { + defaultMessage: 'Shards', + }), + sortKey: 'shardCount' + }); return cols; }; @@ -154,7 +201,7 @@ const nodeRowFactory = showCgroupMetricsElasticsearch => { }; }; -export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElasticsearch, ...props }) { +function ElasticsearchNodesUI({ clusterStatus, nodes, showCgroupMetricsElasticsearch, intl, ...props }) { const columns = getColumns(showCgroupMetricsElasticsearch); return ( @@ -169,7 +216,10 @@ export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElas sortKey={props.sortKey} sortOrder={props.sortOrder} onNewState={props.onNewState} - placeholder="Filter Nodes..." + placeholder={intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder', + defaultMessage: 'Filter Nodes…', + })} filterFields={filterFields} columns={columns} rowComponent={nodeRowFactory(showCgroupMetricsElasticsearch)} @@ -177,3 +227,5 @@ export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElas
); } + +export const ElasticsearchNodes = injectI18n(ElasticsearchNodesUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js index c014b41785e0351..b40938ba3a96a56 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js @@ -5,6 +5,7 @@ */ import React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; export const FilesProgress = ({ filesPercent, filesDone, filesTotal }) => { return ( @@ -30,6 +31,11 @@ export const TranslogProgress = ({ hasTranslog, translogPercent, translogDone, t {translogPercent}
{translogDone} / {translogTotal} - ) : 'n/a'; + ) : ( + + ); }; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js index f33401f06d655ad..45c1a432a491d66 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js @@ -7,6 +7,7 @@ import React, { Fragment } from 'react'; import { EuiLink } from '@elastic/eui'; import { Snapshot } from './snapshot'; +import { FormattedMessage } from '@kbn/i18n/react'; export const RecoveryIndex = (props) => { const { name, shard, relocationType } = props; @@ -14,8 +15,20 @@ export const RecoveryIndex = (props) => { return ( {name}
- Shard: {shard}
- Recovery type: {relocationType} +
+
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js index eec1e8ec4954275..b41424bc6620f7b 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js @@ -18,15 +18,52 @@ import { TotalTime } from './total_time'; import { SourceDestination } from './source_destination'; import { FilesProgress, BytesProgress, TranslogProgress } from './progress'; import { parseProps } from './parse_props'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; const columns = [ - { title: 'Index', sortKey: null }, - { title: 'Stage', sortKey: null }, - { title: 'Total Time', sortKey: null }, - { title: 'Source / Destination', sortKey: null }, - { title: 'Files', sortKey: null }, - { title: 'Bytes', sortKey: null }, - { title: 'Translog', sortKey: null } + { + title: i18n.translate('xpack.monitoring.kibana.shardActivity.indexTitle', { + defaultMessage: 'Index' + }), + sortKey: null + }, + { + title: i18n.translate('xpack.monitoring.kibana.shardActivity.stageTitle', { + defaultMessage: 'Stage' + }), + sortKey: null + }, + { + title: i18n.translate('xpack.monitoring.kibana.shardActivity.totalTimeTitle', { + defaultMessage: 'Total Time' + }), + sortKey: null + }, + { + title: i18n.translate('xpack.monitoring.kibana.shardActivity.sourceDestinationTitle', { + defaultMessage: 'Source / Destination' + }), + sortKey: null + }, + { + title: i18n.translate('xpack.monitoring.kibana.shardActivity.filesTitle', { + defaultMessage: 'Files' + }), + sortKey: null + }, + { + title: i18n.translate('xpack.monitoring.kibana.shardActivity.bytesTitle', { + defaultMessage: 'Bytes' + }), + sortKey: null + }, + { + title: i18n.translate('xpack.monitoring.kibana.shardActivity.translogTitle', { + defaultMessage: 'Translog' + }), + sortKey: null + } ]; const ActivityRow = props => ( @@ -57,7 +94,12 @@ const ToggleCompletedSwitch = ({ toggleHistory, showHistory }) => ( + )} onChange={toggleHistory} checked={showHistory} /> @@ -65,7 +107,7 @@ const ToggleCompletedSwitch = ({ toggleHistory, showHistory }) => ( ); -export class ShardActivity extends React.Component { +class ShardActivityUI extends React.Component { constructor(props) { super(props); this.getNoDataMessage = this.getNoDataMessage.bind(this); @@ -73,12 +115,31 @@ export class ShardActivity extends React.Component { getNoDataMessage() { if (this.props.showShardActivityHistory) { - return 'There are no historical shard activity records for the selected time range.'; + return this.props.intl.formatMessage({ + id: 'xpack.monitoring.elasticsearch.shardActivity.noDataMessage', + defaultMessage: 'There are no historical shard activity records for the selected time range.' + }); } return ( - There are no active shard recoveries for this cluster.
- Try viewing completed recoveries. +
+ + + + ) + }} + />
); } @@ -102,7 +163,12 @@ export class ShardActivity extends React.Component { -

Shard Activity

+

+ +

@@ -120,3 +186,5 @@ export class ShardActivity extends React.Component { ); } } + +export const ShardActivity = injectI18n(ShardActivityUI); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js index be7b5daece091a0..ece2fae9b7a7acd 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js @@ -5,11 +5,19 @@ */ import React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; export const Snapshot = ({ isSnapshot, repo, snapshot }) => { return isSnapshot ? ( - Repo: {repo} / Snapshot: {snapshot} + ) : null; }; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js index 440cf56fde33fc0..0325d077cab8241 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js @@ -7,6 +7,7 @@ import React, { Fragment } from 'react'; import { EuiLink } from '@elastic/eui'; import { Tooltip } from 'plugins/monitoring/components/tooltip'; +import { FormattedMessage } from '@kbn/i18n/react'; export const SourceTooltip = ({ isCopiedFromPrimary, sourceTransportAddress, children }) => { if (!sourceTransportAddress) { @@ -17,7 +18,24 @@ export const SourceTooltip = ({ isCopiedFromPrimary, sourceTransportAddress, chi {sourceTransportAddress}
- Copied from { isCopiedFromPrimary ? 'primary' : 'replica' } shard + + ) : ( + + ), + }} + />
); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js index c55531dfdee7c0c..a456646218f0637 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js @@ -7,10 +7,23 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; import { Tooltip } from 'plugins/monitoring/components/tooltip'; +import { FormattedMessage } from '@kbn/i18n/react'; export const TotalTime = ({ startTime, totalTime }) => { return ( - + + } + placement="bottom" + trigger="hover" + > {totalTime} ); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js index 623fbbfde30a683..0015071d0822813 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js @@ -6,14 +6,25 @@ import React from 'react'; import { StatusIcon } from '../status_icon'; +import { injectI18n } from '@kbn/i18n/react'; -export function ElasticsearchStatusIcon({ status }) { +function ElasticsearchStatusIconUI({ intl, status }) { const type = (() => { const statusKey = status.toUpperCase(); return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.GRAY; })(); return ( - + ); } + +export const ElasticsearchStatusIcon = injectI18n(ElasticsearchStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js index 20fa1fcbdee0090..28b2dbbc87ca678 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js @@ -8,8 +8,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { KibanaStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; +import { injectI18n } from '@kbn/i18n/react'; -export function ClusterStatus({ stats }) { +function ClusterStatusUI({ stats, intl }) { const { concurrent_connections: connections, count: instances, @@ -22,27 +23,42 @@ export function ClusterStatus({ stats }) { const metrics = [ { - label: 'Instances', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.clusterStatus.instancesLabel', + defaultMessage: 'Instances' + }), value: instances, dataTestSubj: 'instances' }, { - label: 'Memory', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.clusterStatus.memoryLabel', + defaultMessage: 'Memory' + }), value: formatMetric(memSize, 'byte') + ' / ' + formatMetric(memLimit, 'byte'), dataTestSubj: 'memory' }, { - label: 'Requests', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.clusterStatus.requestsLabel', + defaultMessage: 'Requests' + }), value: requests, dataTestSubj: 'requests' }, { - label: 'Connections', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.clusterStatus.connectionsLabel', + defaultMessage: 'Connections' + }), value: connections, dataTestSubj: 'connections' }, { - label: 'Max. Response Time', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.clusterStatus.maxResponseTimeLabel', + defaultMessage: 'Max. Response Time' + }), value: formatMetric(maxResponseTime, '0', 'ms'), dataTestSubj: 'maxResponseTime' } @@ -61,3 +77,5 @@ export function ClusterStatus({ stats }) { /> ); } + +export const ClusterStatus = injectI18n(ClusterStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js index bffe4082a2b624b..b8b4cd2ce498bdd 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js @@ -8,8 +8,9 @@ import React from 'react'; import { SummaryStatus } from '../../summary_status'; import { KibanaStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; +import { injectI18n } from '@kbn/i18n/react'; -export function DetailStatus({ stats }) { +function DetailStatusUI({ stats, intl }) { const { transport_address: transportAddress, os_memory_free: osFreeMemory, @@ -24,17 +25,26 @@ export function DetailStatus({ stats }) { dataTestSubj: 'transportAddress' }, { - label: 'OS Free Memory', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.detailStatus.osFreeMemoryLabel', + defaultMessage: 'OS Free Memory' + }), value: formatMetric(osFreeMemory, 'byte'), dataTestSubj: 'osFreeMemory' }, { - label: 'Version', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.detailStatus.versionLabel', + defaultMessage: 'Version' + }), value: version, dataTestSubj: 'version' }, { - label: 'Uptime', + label: intl.formatMessage({ + id: 'xpack.monitoring.kibana.detailStatus.uptimeLabel', + defaultMessage: 'Uptime' + }), value: formatMetric(uptime, 'time_since'), dataTestSubj: 'uptime' } @@ -53,3 +63,5 @@ export function DetailStatus({ stats }) { /> ); } + +export const DetailStatus = injectI18n(DetailStatusUI); diff --git a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js index a9fa9e163289f7c..6ce9f2dc5bea668 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js @@ -6,8 +6,9 @@ import React from 'react'; import { StatusIcon } from 'plugins/monitoring/components/status_icon'; +import { injectI18n } from '@kbn/i18n/react'; -export function KibanaStatusIcon({ status, availability = true }) { +function KibanaStatusIconUI({ status, availability = true, intl }) { const type = (() => { if (!availability) { return StatusIcon.TYPES.GRAY; @@ -18,6 +19,15 @@ export function KibanaStatusIcon({ status, availability = true }) { })(); return ( - + ); } + +export const KibanaStatusIcon = injectI18n(KibanaStatusIconUI); diff --git a/x-pack/plugins/monitoring/public/directives/alerts/index.js b/x-pack/plugins/monitoring/public/directives/alerts/index.js index 22efffd61dcfce7..d125b0f1c074b29 100644 --- a/x-pack/plugins/monitoring/public/directives/alerts/index.js +++ b/x-pack/plugins/monitoring/public/directives/alerts/index.js @@ -17,25 +17,65 @@ import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_a import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../common/format_timestamp_to_duration'; import { formatDateTimeLocal } from '../../../common/formatting'; -import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { injectI18n, I18nProvider, FormattedMessage } from '@kbn/i18n/react'; const linkToCategories = { - 'elasticsearch/nodes': 'Elasticsearch Nodes', - 'elasticsearch/indices': 'Elasticsearch Indices', - 'kibana/instances': 'Kibana Instances', - 'logstash/instances': 'Logstash Nodes', + 'elasticsearch/nodes': i18n.translate('xpack.monitoring.alerts.esNodesCategoryLabel', { + defaultMessage: 'Elasticsearch Nodes', + }), + 'elasticsearch/indices': i18n.translate('xpack.monitoring.alerts.esIndicesCategoryLabel', { + defaultMessage: 'Elasticsearch Indices', + }), + 'kibana/instances': i18n.translate('xpack.monitoring.alerts.kibanaInstancesCategoryLabel', { + defaultMessage: 'Kibana Instances', + }), + 'logstash/instances': i18n.translate('xpack.monitoring.alerts.logstashNodesCategoryLabel', { + defaultMessage: 'Logstash Nodes', + }), }; const filterFields = [ 'message', 'severity_group', 'prefix', 'suffix', 'metadata.link', 'since', 'timestamp', 'update_timestamp' ]; const columns = [ - { title: 'Status', sortKey: 'metadata.severity', sortOrder: SORT_DESCENDING }, - { title: 'Resolved', sortKey: 'resolved_timestamp' }, - { title: 'Message', sortKey: 'message' }, - { title: 'Category', sortKey: 'metadata.link' }, - { title: 'Last Checked', sortKey: 'update_timestamp' }, - { title: 'Triggered', sortKey: 'timestamp' }, + { + title: i18n.translate('xpack.monitoring.alerts.statusColumnTitle', { + defaultMessage: 'Status', + }), + sortKey: 'metadata.severity', + sortOrder: SORT_DESCENDING + }, + { + title: i18n.translate('xpack.monitoring.alerts.resolvedColumnTitle', { + defaultMessage: 'Resolved', + }), + sortKey: 'resolved_timestamp' + }, + { + title: i18n.translate('xpack.monitoring.alerts.messageColumnTitle', { + defaultMessage: 'Message', + }), + sortKey: 'message' + }, + { + title: i18n.translate('xpack.monitoring.alerts.categoryColumnTitle', { + defaultMessage: 'Category', + }), + sortKey: 'metadata.link' + }, + { + title: i18n.translate('xpack.monitoring.alerts.lastCheckedColumnTitle', { + defaultMessage: 'Last Checked', + }), + sortKey: 'update_timestamp' + }, + { + title: i18n.translate('xpack.monitoring.alerts.triggeredColumnTitle', { + defaultMessage: 'Triggered', + }), + sortKey: 'timestamp' + }, ]; const alertRowFactory = (scope, kbnUrl) => { - return props => { + return injectI18n(props => { const changeUrl = target => { scope.$evalAsync(() => { kbnUrl.changePath(target); @@ -44,13 +84,24 @@ const alertRowFactory = (scope, kbnUrl) => { const severityIcon = mapSeverity(props.metadata.severity); const resolution = { icon: null, - text: 'Not Resolved' + text: props.intl.formatMessage({ id: 'xpack.monitoring.alerts.notResolvedDescription', + defaultMessage: 'Not Resolved', + }) }; if (props.resolved_timestamp) { - resolution.text = `${formatTimestampToDuration(props.resolved_timestamp, CALCULATE_DURATION_SINCE)} ago`; + resolution.text = props.intl.formatMessage({ id: 'xpack.monitoring.alerts.resolvedAgoDescription', + defaultMessage: '{duration} ago', + }, { duration: formatTimestampToDuration(props.resolved_timestamp, CALCULATE_DURATION_SINCE) } + ); } else { - resolution.icon = ; + resolution.icon = ( + + ); } return ( @@ -75,25 +126,31 @@ const alertRowFactory = (scope, kbnUrl) => { /> - { linkToCategories[props.metadata.link] ? linkToCategories[props.metadata.link] : 'General' } + { linkToCategories[props.metadata.link] ? linkToCategories[props.metadata.link] : + props.intl.formatMessage({ id: 'xpack.monitoring.alerts.generalCategoryLabel', defaultMessage: 'General', }) } { formatDateTimeLocal(props.update_timestamp) } - { formatTimestampToDuration(props.timestamp, CALCULATE_DURATION_SINCE) } ago +
); - }; + }); }; const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringClusterAlertsListing', kbnUrl => { +uiModule.directive('monitoringClusterAlertsListing', (kbnUrl, i18n) => { return { restrict: 'E', scope: { alerts: '=' }, link(scope, $el) { + const filterAlertsPlaceholder = i18n('xpack.monitoring.alerts.filterAlertsPlaceholder', { defaultMessage: 'Filter Alerts…' }); scope.$watch('alerts', (alerts = []) => { const alertsTable = ( @@ -101,7 +158,7 @@ uiModule.directive('monitoringClusterAlertsListing', kbnUrl => { { @@ -61,14 +102,36 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => handleClickIncompatibleLicense() { this.licenseWarning({ - title: `You can't view the "${this.props.cluster_name}" cluster`, + title: ( + + ), text: ( -

The Basic license does not support multi-cluster monitoring.

- Need to monitor multiple clusters?{' '} - Get a license with full functionality{' '} - to enjoy multi-cluster monitoring. + +

+

+ + + + ) + }} + />

), @@ -79,15 +142,44 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => const licensingPath = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management/home`; this.licenseWarning({ - title: `You can't view the "${this.props.cluster_name}" cluster`, + title: ( + + ), text: ( -

The license information is invalid.

- Need a license?{' '} - Get a free Basic license or{' '} - get a license with full functionality{' '} - to enjoy multi-cluster monitoring. + +

+

+ + + + ), + getLicenseInfoLink: ( + + + + ) + }} + />

), @@ -136,7 +228,10 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => // license is expired return ( - Expired + ); } @@ -144,7 +239,11 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => // license is fine return ( - Expires { moment(this.props.license.expiry_date_in_millis).format('D MMM YY') } + ); }; @@ -167,7 +266,10 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => onClick={this.handleClickInvalidLicense.bind(this)} > - N/A + ); @@ -212,7 +314,10 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => trigger="hover" > - N/A + ); @@ -268,7 +373,7 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => }; const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringClusterListing', ($injector) => { +uiModule.directive('monitoringClusterListing', ($injector, i18n) => { return { restrict: 'E', scope: { @@ -283,6 +388,9 @@ uiModule.directive('monitoringClusterListing', ($injector) => { const globalState = $injector.get('globalState'); const kbnUrl = $injector.get('kbnUrl'); const showLicenseExpiration = $injector.get('showLicenseExpiration'); + const filterClustersPlaceholder = i18n('xpack.monitoring.cluster.listing.filterClustersPlaceholder', + { defaultMessage: 'Filter Clusters…' } + ); scope.$watch('clusters', (clusters = []) => { const clusterTable = ( @@ -295,7 +403,7 @@ uiModule.directive('monitoringClusterListing', ($injector) => { sortKey={scope.sortKey} sortOrder={scope.sortOrder} onNewState={scope.onNewState} - placeholder="Filter Clusters..." + placeholder={filterClustersPlaceholder} filterFields={filterFields} columns={columns} rowComponent={clusterRowFactory(scope, globalState, kbnUrl, showLicenseExpiration)} diff --git a/x-pack/plugins/monitoring/public/directives/cluster/overview/index.js b/x-pack/plugins/monitoring/public/directives/cluster/overview/index.js index 91952c4a9675ce5..17439f890886ea8 100644 --- a/x-pack/plugins/monitoring/public/directives/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/directives/cluster/overview/index.js @@ -8,6 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Overview } from 'plugins/monitoring/components/cluster/overview'; import { uiModules } from 'ui/modules'; +import { I18nProvider } from '@kbn/i18n/react'; const uiModule = uiModules.get('monitoring/directives', []); uiModule.directive('monitoringClusterOverview', (kbnUrl, showLicenseExpiration) => { @@ -24,11 +25,13 @@ uiModule.directive('monitoringClusterOverview', (kbnUrl, showLicenseExpiration) scope.$watch('cluster', cluster => { ReactDOM.render(( - + + + ), $el[0]); }); diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/cluster_status/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/cluster_status/index.js index 0be64b18406a846..7f0d5a665c334c4 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/cluster_status/index.js @@ -8,6 +8,7 @@ import React from 'react'; import { render } from 'react-dom'; import { uiModules } from 'ui/modules'; import { ClusterStatus } from 'plugins/monitoring/components/elasticsearch/cluster_status'; +import { I18nProvider } from '@kbn/i18n/react'; const uiModule = uiModules.get('monitoring/directives', []); uiModule.directive('monitoringClusterStatusElasticsearch', () => { @@ -18,7 +19,7 @@ uiModule.directive('monitoringClusterStatusElasticsearch', () => { }, link(scope, $el) { scope.$watch('status', status => { - render(, $el[0]); + render(, $el[0]); }); } }; diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/index_summary/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/index_summary/index.js index e1fa222a0ee80d8..24e48609c478179 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/index_summary/index.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/index_summary/index.js @@ -8,6 +8,7 @@ import React from 'react'; import { render } from 'react-dom'; import { uiModules } from 'ui/modules'; import { IndexDetailStatus } from 'plugins/monitoring/components/elasticsearch/index_detail_status'; +import { I18nProvider } from '@kbn/i18n/react'; const uiModule = uiModules.get('monitoring/directives', []); uiModule.directive('monitoringIndexSummary', () => { @@ -16,7 +17,7 @@ uiModule.directive('monitoringIndexSummary', () => { scope: { summary: '=' }, link(scope, $el) { scope.$watch('summary', summary => { - render(, $el[0]); + render(, $el[0]); }); } }; diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js index 1de1861b52bfc55..2fac710fef7f42d 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js @@ -20,16 +20,48 @@ import { LARGE_ABBREVIATED, LARGE_BYTES } from '../../../../common/formatting'; import { EuiLink, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; const filterFields = [ 'job_id', 'state', 'node.name' ]; const columns = [ - { title: 'Job ID', sortKey: 'job_id', sortOrder: SORT_ASCENDING }, - { title: 'State', sortKey: 'state' }, - { title: 'Processed Records', sortKey: 'data_counts.processed_record_count' }, - { title: 'Model Size', sortKey: 'model_size_stats.model_bytes' }, - { title: 'Forecasts', sortKey: 'forecasts_stats.total' }, - { title: 'Node', sortKey: 'node.name' } + { + title: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.jobIdTitle', { + defaultMessage: 'Job ID' + }), + sortKey: 'job_id', + sortOrder: SORT_ASCENDING + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.stateTitle', { + defaultMessage: 'State' + }), + sortKey: 'state' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.processedRecordsTitle', { + defaultMessage: 'Processed Records' + }), + sortKey: 'data_counts.processed_record_count' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.modelSizeTitle', { + defaultMessage: 'Model Size' + }), + sortKey: 'model_size_stats.model_bytes' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.forecastsTitle', { + defaultMessage: 'Forecasts' + }), + sortKey: 'forecasts_stats.total' + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.nodeTitle', { + defaultMessage: 'Node' + }), + sortKey: 'node.name' + } ]; const jobRowFactory = (scope, kbnUrl) => { const goToNode = nodeId => { @@ -45,7 +77,9 @@ const jobRowFactory = (scope, kbnUrl) => { ); } - return 'N/A'; + return i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.noDataLabel', { + defaultMessage: 'N/A' + }); }; return function JobRow(props) { @@ -68,7 +102,7 @@ const jobRowFactory = (scope, kbnUrl) => { }; const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringMlListing', kbnUrl => { +uiModule.directive('monitoringMlListing', (kbnUrl, i18n) => { return { restrict: 'E', scope: { @@ -84,13 +118,24 @@ uiModule.directive('monitoringMlListing', kbnUrl => { const getNoDataMessage = filterText => { if (filterText) { return ( - `There are no Machine Learning Jobs that match the filter [${filterText.trim()}] or the time range. -Try changing the filter or time range.` + i18n('xpack.monitoring.elasticsearch.mlJobListing.noFilteredJobsDescription', { + // eslint-disable-next-line max-len + defaultMessage: 'There are no Machine Learning Jobs that match the filter [{filterText}] or the time range. Try changing the filter or time range.', + values: { + filterText: filterText.trim() + } + }) ); } - return 'There are no Machine Learning Jobs that match your query. Try changing the time range selection.'; + return i18n('xpack.monitoring.elasticsearch.mlJobListing.noJobsDescription', { + defaultMessage: 'There are no Machine Learning Jobs that match your query. Try changing the time range selection.' + }); }; + const filterJobsPlaceholder = i18n('xpack.monitoring.elasticsearch.mlJobListing.filterJobsPlaceholder', { + defaultMessage: 'Filter Jobs…' + }); + scope.$watch('jobs', (jobs = []) => { const mlTable = ( @@ -102,7 +147,7 @@ Try changing the filter or time range.` sortKey={scope.sortKey} sortOrder={scope.sortOrder} onNewState={scope.onNewState} - placeholder="Filter Jobs..." + placeholder={filterJobsPlaceholder} filterFields={filterFields} columns={columns} rowComponent={jobRowFactory(scope, kbnUrl)} diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/node_summary/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/node_summary/index.js index 638e2423233ef41..81a8a55f58b0af4 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/node_summary/index.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/node_summary/index.js @@ -8,6 +8,7 @@ import React from 'react'; import { render } from 'react-dom'; import { uiModules } from 'ui/modules'; import { NodeDetailStatus } from 'plugins/monitoring/components/elasticsearch/node_detail_status'; +import { I18nProvider } from '@kbn/i18n/react'; const uiModule = uiModules.get('monitoring/directives', []); uiModule.directive('monitoringNodeSummary', () => { @@ -18,7 +19,7 @@ uiModule.directive('monitoringNodeSummary', () => { }, link(scope, $el) { scope.$watch('node', node => { - render(, $el[0]); + render(, $el[0]); }); } }; diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/clusterView.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/clusterView.js index b6863de16645b4e..f9cf7d57ac1ac14 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/clusterView.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/clusterView.js @@ -9,9 +9,12 @@ import React from 'react'; import { TableHead } from './tableHead'; import { TableBody } from './tableBody'; +import { i18n } from '@kbn/i18n'; export class ClusterView extends React.Component { - static displayName = 'ClusterView'; + static displayName = i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.clusterViewDisplayName', { + defaultMessage: 'ClusterView', + }); constructor(props) { super(props); diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/shard.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/shard.js index bc8b3617ed815c0..ec41258263ed79d 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/shard.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/shard.js @@ -9,9 +9,12 @@ import React from 'react'; import { calculateClass } from '../lib/calculateClass'; import { vents } from '../lib/vents'; +import { i18n } from '@kbn/i18n'; export class Shard extends React.Component { - static displayName = 'Shard'; + static displayName = i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.shardDisplayName', { + defaultMessage: 'Shard', + }); state = { tooltipVisible: false }; componentDidMount() { diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableBody.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableBody.js index 2d72ae7ab969231..b375a5eec3d33ab 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableBody.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableBody.js @@ -9,6 +9,8 @@ import React from 'react'; import { Unassigned } from './unassigned'; import { Assigned } from './assigned'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; const ShardRow = props => { let unassigned; @@ -34,7 +36,9 @@ const ShardRow = props => { }; export class TableBody extends React.Component { - static displayName = 'TableBody'; + static displayName = i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.tableBodyDisplayName', { + defaultMessage: 'TableBody', + }); createRow = (data, index) => { return ( @@ -55,7 +59,10 @@ export class TableBody extends React.Component {

- There are no shards allocated. +

diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableHead.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableHead.js index 1c11f38ebccb86a..3a11b1e4c107f81 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableHead.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/components/tableHead.js @@ -12,6 +12,7 @@ import { EuiFlexItem, EuiSwitch, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; class IndexLabel extends React.Component { @@ -38,7 +39,10 @@ class IndexLabel extends React.Component { - Indices + { const type = shard.primary ? 'primary' : 'replica'; diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/directives/clusterView.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/directives/clusterView.js index 9244c6e80725132..83e052c3f5b5237 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/directives/clusterView.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/directives/clusterView.js @@ -10,6 +10,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { ClusterView } from '../components/clusterView'; import { uiModules } from 'ui/modules'; +import { I18nProvider } from '@kbn/i18n/react'; const uiModule = uiModules.get('monitoring/directives', []); uiModule.directive('clusterView', kbnUrl => { @@ -26,12 +27,14 @@ uiModule.directive('clusterView', kbnUrl => { }, link: function (scope, element) { ReactDOM.render( - , + + + , element[0] ); } diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/index.html b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/index.html index a681ba0fc81c69e..f62b70882e90f78 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/index.html +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/shard_allocation/index.html @@ -1,19 +1,49 @@
-

Shard Legend

+

  - Primary +   - Replica +   - Relocating +   - Initializing +   - Unassigned Primary +   - Unassigned Replica +
{ @@ -18,7 +19,7 @@ uiModule.directive('monitoringClusterStatusKibana', () => { }, link(scope, $el) { scope.$watch('status', status => { - render(, $el[0]); + render(, $el[0]); }); }, }; diff --git a/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js b/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js index 62e1f9acc33dd35..46c2dca3468d667 100644 --- a/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js +++ b/x-pack/plugins/monitoring/public/directives/kibana/listing/index.js @@ -22,16 +22,48 @@ import { import { EuiLink, } from '@elastic/eui'; -import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, injectI18n, I18nProvider } from '@kbn/i18n/react'; const filterFields = [ 'kibana.name', 'kibana.host', 'kibana.status', 'kibana.transport_address' ]; const columns = [ - { title: 'Name', sortKey: 'kibana.name', sortOrder: SORT_ASCENDING }, - { title: 'Status', sortKey: 'kibana.status' }, - { title: 'Load Average', sortKey: 'os.load.1m' }, - { title: 'Memory Size', sortKey: 'process.memory.resident_set_size_in_bytes' }, - { title: 'Requests', sortKey: 'requests.total' }, - { title: 'Response Times', sortKey: 'response_times.average' } + { + title: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', { + defaultMessage: 'Name' + }), + sortKey: 'kibana.name', + sortOrder: SORT_ASCENDING + }, + { + title: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', { + defaultMessage: 'Status' + }), + sortKey: 'kibana.status' + }, + { + title: i18n.translate('xpack.monitoring.kibana.listing.loadAverageColumnTitle', { + defaultMessage: 'Load Average' + }), + sortKey: 'os.load.1m' + }, + { + title: i18n.translate('xpack.monitoring.kibana.listing.memorySizeColumnTitle', { + defaultMessage: 'Memory Size' + }), + sortKey: 'process.memory.resident_set_size_in_bytes' + }, + { + title: i18n.translate('xpack.monitoring.kibana.listing.requestsColumnTitle', { + defaultMessage: 'Requests' + }), + sortKey: 'requests.total' + }, + { + title: i18n.translate('xpack.monitoring.kibana.listing.responseTimeColumnTitle', { + defaultMessage: 'Response Times' + }), + sortKey: 'response_times.average' + }, ]; const instanceRowFactory = (scope, kbnUrl) => { const goToInstance = uuid => { @@ -40,7 +72,7 @@ const instanceRowFactory = (scope, kbnUrl) => { }); }; - return function KibanaRow(props) { + return injectI18n(function KibanaRow(props) { return ( @@ -55,9 +87,25 @@ const instanceRowFactory = (scope, kbnUrl) => {
{ get(props, 'kibana.transport_address') }
-
+
  - { !props.availability ? 'Offline' : capitalize(props.kibana.status) } + { !props.availability ? ( + + ) : capitalize(props.kibana.status) }
@@ -85,11 +133,11 @@ const instanceRowFactory = (scope, kbnUrl) => { ); - }; + }); }; const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringKibanaListing', kbnUrl => { +uiModule.directive('monitoringKibanaListing', (kbnUrl, i18n) => { return { restrict: 'E', scope: { @@ -101,6 +149,9 @@ uiModule.directive('monitoringKibanaListing', kbnUrl => { onNewState: '=', }, link(scope, $el) { + const filterInstancesPlaceholder = i18n('xpack.monitoring.kibana.listing.filterInstancesPlaceholder', { + defaultMessage: 'Filter Instances…' + }); scope.$watch('instances', (instances = []) => { const kibanasTable = ( @@ -113,7 +164,7 @@ uiModule.directive('monitoringKibanaListing', kbnUrl => { sortKey={scope.sortKey} sortOrder={scope.sortOrder} onNewState={scope.onNewState} - placeholder="Filter Instances..." + placeholder={filterInstancesPlaceholder} filterFields={filterFields} columns={columns} rowComponent={instanceRowFactory(scope, kbnUrl)} diff --git a/x-pack/plugins/monitoring/public/directives/kibana/summary/index.js b/x-pack/plugins/monitoring/public/directives/kibana/summary/index.js index 1077ed579420a05..521fc37a650ce98 100644 --- a/x-pack/plugins/monitoring/public/directives/kibana/summary/index.js +++ b/x-pack/plugins/monitoring/public/directives/kibana/summary/index.js @@ -8,6 +8,7 @@ import React from 'react'; import { render } from 'react-dom'; import { uiModules } from 'ui/modules'; import { DetailStatus } from 'plugins/monitoring/components/kibana/detail_status'; +import { I18nProvider } from '@kbn/i18n/react'; const uiModule = uiModules.get('monitoring/directives', []); uiModule.directive('monitoringKibanaSummary', () => { @@ -18,7 +19,7 @@ uiModule.directive('monitoringKibanaSummary', () => { }, link(scope, $el) { scope.$watch('kibana', kibana => { - render(, $el[0]); + render(, $el[0]); }); } }; diff --git a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js index 552dc49e633280e..8bad9b9221580aa 100644 --- a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js +++ b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.js @@ -13,6 +13,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; export function formatMonitoringError(err) { // TODO: We should stop using Boom for errors and instead write a custom handler to return richer error objects @@ -24,7 +25,11 @@ export function formatMonitoringError(err) { { err.data.message }

- HTTP { err.status } + ); @@ -43,7 +48,11 @@ export function ajaxErrorHandlersProvider($injector) { kbnUrl.redirect('access-denied'); } else if (err.status === 404 && !contains($window.location.hash, 'no-data')) { // pass through if this is a 404 and we're already on the no-data page toastNotifications.addDanger({ - title: 'Monitoring Request Failed', + title: ( + ), text: (
{ formatMonitoringError(err) } @@ -53,14 +62,21 @@ export function ajaxErrorHandlersProvider($injector) { color="danger" onClick={() => $window.location.reload()} > - Retry +
) }); } else { toastNotifications.addDanger({ - title: 'Monitoring Request Error', + title: ( + ), text: formatMonitoringError(err) }); } diff --git a/x-pack/plugins/monitoring/public/lib/format_number.js b/x-pack/plugins/monitoring/public/lib/format_number.js index 3ef93e9140cbb0c..38638736e8cf986 100644 --- a/x-pack/plugins/monitoring/public/lib/format_number.js +++ b/x-pack/plugins/monitoring/public/lib/format_number.js @@ -7,6 +7,7 @@ import moment from 'moment'; import 'moment-duration-format'; import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; export function formatBytesUsage(used, max) { return formatNumber(used, 'byte') + ' / ' + formatNumber(max, 'byte'); @@ -61,5 +62,5 @@ export function formatMetric(value, format, suffix, options = {}) { } return formatNumber(value, format) + _suffix; } - return 'N/A'; + return i18n.translate('xpack.monitoring.formatNumbers.notAvailableLabel', { defaultMessage: 'N/A' }); } diff --git a/x-pack/plugins/monitoring/public/register_feature.js b/x-pack/plugins/monitoring/public/register_feature.js index ccfd85f9f67b3be..bef65ec24486080 100644 --- a/x-pack/plugins/monitoring/public/register_feature.js +++ b/x-pack/plugins/monitoring/public/register_feature.js @@ -10,11 +10,15 @@ import chrome from 'ui/chrome'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; if (chrome.getInjected('monitoringUiEnabled')) { - FeatureCatalogueRegistryProvider.register(() => { + FeatureCatalogueRegistryProvider.register((i18n) => { return { id: 'monitoring', - title: 'Monitoring', - description: 'Track the real-time health and performance of your Elastic Stack.', + title: i18n('xpack.monitoring.monitoringTitle', { + defaultMessage: 'Monitoring' + }), + description: i18n('xpack.monitoring.monitoringDescription', { + defaultMessage: 'Track the real-time health and performance of your Elastic Stack.' + }), icon: 'monitoringApp', path: '/app/monitoring', showOnHomePage: true, diff --git a/x-pack/plugins/monitoring/public/services/breadcrumbs_provider.js b/x-pack/plugins/monitoring/public/services/breadcrumbs_provider.js index fbc7f596a7af3f6..8d3adcb541e9138 100644 --- a/x-pack/plugins/monitoring/public/services/breadcrumbs_provider.js +++ b/x-pack/plugins/monitoring/public/services/breadcrumbs_provider.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set as setBreadcrumbs } from 'ui/chrome/services/breadcrumb_state'; +import chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; // Helper for making objects to use in a link element const createCrumb = (url, label, testSubj) => { @@ -21,14 +22,20 @@ function getElasticsearchBreadcrumbs(mainInstance) { if (mainInstance.instance) { breadcrumbs.push(createCrumb('#/elasticsearch', 'Elasticsearch')); if (mainInstance.name === 'indices') { - breadcrumbs.push(createCrumb('#/elasticsearch/indices', 'Indices', 'breadcrumbEsIndices')); + breadcrumbs.push(createCrumb('#/elasticsearch/indices', i18n.translate( + 'xpack.monitoring.breadcrumbs.es.indicesLabel', { defaultMessage: 'Indices' }), 'breadcrumbEsIndices')); } else if (mainInstance.name === 'nodes') { - breadcrumbs.push(createCrumb('#/elasticsearch/nodes', 'Nodes', 'breadcrumbEsNodes')); + breadcrumbs.push(createCrumb('#/elasticsearch/nodes', i18n.translate( + 'xpack.monitoring.breadcrumbs.es.nodesLabel', { defaultMessage: 'Nodes' }), 'breadcrumbEsNodes')); } else if (mainInstance.name === 'ml') { // ML Instance (for user later) - breadcrumbs.push(createCrumb('#/elasticsearch/ml_jobs', 'Jobs')); + breadcrumbs.push(createCrumb('#/elasticsearch/ml_jobs', i18n.translate( + 'xpack.monitoring.breadcrumbs.es.jobsLabel', { defaultMessage: 'Jobs' }) + )); } else if (mainInstance.name === 'ccr_shard') { - breadcrumbs.push(createCrumb('#/elasticsearch/ccr', 'CCR')); + breadcrumbs.push(createCrumb('#/elasticsearch/ccr', i18n.translate( + 'xpack.monitoring.breadcrumbs.es.ccrLabel', { defaultMessage: 'CCR' }) + )); } breadcrumbs.push(createCrumb(null, mainInstance.instance)); } else { @@ -43,7 +50,9 @@ function getKibanaBreadcrumbs(mainInstance) { const breadcrumbs = []; if (mainInstance.instance) { breadcrumbs.push(createCrumb('#/kibana', 'Kibana')); - breadcrumbs.push(createCrumb('#/kibana/instances', 'Instances')); + breadcrumbs.push(createCrumb('#/kibana/instances', i18n.translate( + 'xpack.monitoring.breadcrumbs.kibana.instancesLabel', { defaultMessage: 'Instances' }) + )); } else { // don't link to Overview when we're possibly on Overview or its sibling tabs breadcrumbs.push(createCrumb(null, 'Kibana')); @@ -53,19 +62,24 @@ function getKibanaBreadcrumbs(mainInstance) { // generate Logstash breadcrumbs function getLogstashBreadcrumbs(mainInstance) { + const logstashLabel = i18n.translate('xpack.monitoring.breadcrumbs.logstashLabel', { defaultMessage: 'Logstash' }); const breadcrumbs = []; if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/logstash', 'Logstash')); + breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); if (mainInstance.name === 'nodes') { - breadcrumbs.push(createCrumb('#/logstash/nodes', 'Nodes')); + breadcrumbs.push(createCrumb('#/logstash/nodes', i18n.translate( + 'xpack.monitoring.breadcrumbs.logstash.nodesLabel', { defaultMessage: 'Nodes' }) + )); } breadcrumbs.push(createCrumb(null, mainInstance.instance)); } else if (mainInstance.page === 'pipeline') { - breadcrumbs.push(createCrumb('#/logstash', 'Logstash')); - breadcrumbs.push(createCrumb('#/logstash/pipelines', 'Pipelines')); + breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); + breadcrumbs.push(createCrumb('#/logstash/pipelines', i18n.translate( + 'xpack.monitoring.breadcrumbs.logstash.pipelinesLabel', { defaultMessage: 'Pipelines' }) + )); } else { // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, 'Logstash')); + breadcrumbs.push(createCrumb(null, logstashLabel)); } return breadcrumbs; @@ -73,13 +87,16 @@ function getLogstashBreadcrumbs(mainInstance) { // generate Beats breadcrumbs function getBeatsBreadcrumbs(mainInstance) { + const beatsLabel = i18n.translate('xpack.monitoring.breadcrumbs.beatsLabel', { defaultMessage: 'Beats' }); const breadcrumbs = []; if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/beats', 'Beats')); - breadcrumbs.push(createCrumb('#/beats/beats', 'Instances')); + breadcrumbs.push(createCrumb('#/beats', beatsLabel)); + breadcrumbs.push(createCrumb('#/beats/beats', i18n.translate( + 'xpack.monitoring.breadcrumbs.beats.instancesLabel', { defaultMessage: 'Instances' }) + )); breadcrumbs.push(createCrumb(null, mainInstance.instance)); } else { - breadcrumbs.push(createCrumb(null, 'Beats')); + breadcrumbs.push(createCrumb(null, beatsLabel)); } return breadcrumbs; @@ -87,20 +104,28 @@ function getBeatsBreadcrumbs(mainInstance) { // generate Apm breadcrumbs function getApmBreadcrumbs(mainInstance) { + const apmLabel = i18n.translate('xpack.monitoring.breadcrumbs.apmLabel', { defaultMessage: 'APM' }); const breadcrumbs = []; if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/apm', 'APM')); - breadcrumbs.push(createCrumb('#/apm/instances', 'Instances')); + breadcrumbs.push(createCrumb('#/apm', apmLabel)); + breadcrumbs.push(createCrumb('#/apm/instances', i18n.translate( + 'xpack.monitoring.breadcrumbs.apm.instancesLabel', { defaultMessage: 'Instances' }) + )); } else { // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, 'APM')); + breadcrumbs.push(createCrumb(null, apmLabel)); } return breadcrumbs; } export function breadcrumbsProvider() { return function createBreadcrumbs(clusterName, mainInstance) { - let breadcrumbs = [ createCrumb('#/home', 'Clusters', 'breadcrumbClusters') ]; + let breadcrumbs = [ createCrumb('#/home', + i18n.translate( + 'xpack.monitoring.breadcrumbs.clustersLabel', { defaultMessage: 'Clusters' } + ), + 'breadcrumbClusters') + ]; if (!mainInstance.inOverview && clusterName) { breadcrumbs.push(createCrumb('#/overview', clusterName)); @@ -122,7 +147,7 @@ export function breadcrumbsProvider() { breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance)); } - setBreadcrumbs(breadcrumbs.map(b => ({ text: b.label, href: b.url }))); + chrome.breadcrumbs.set(breadcrumbs.map(b => ({ text: b.label, href: b.url }))); return breadcrumbs; }; diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js index fad5914f1117f47..21e994ce970a2e7 100644 --- a/x-pack/plugins/monitoring/public/services/title.js +++ b/x-pack/plugins/monitoring/public/services/title.js @@ -9,12 +9,16 @@ import { uiModules } from 'ui/modules'; import { DocTitleProvider } from 'ui/doc_title'; const uiModule = uiModules.get('monitoring/title', []); -uiModule.service('title', (Private) => { +uiModule.service('title', (Private, i18n) => { const docTitle = Private(DocTitleProvider); return function changeTitle(cluster, suffix) { let clusterName = _.get(cluster, 'cluster_name'); clusterName = (clusterName) ? `- ${clusterName}` : ''; suffix = (suffix) ? `- ${suffix}` : ''; - docTitle.change(`Monitoring ${clusterName} ${suffix}`, true); + docTitle.change( + i18n('xpack.monitoring.monitoringDocTitle', { + defaultMessage: 'Monitoring {clusterName} {suffix}', + values: { clusterName, suffix } + }), true); }; }); diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.html b/x-pack/plugins/monitoring/public/views/access_denied/index.html index 5f0f0813ff56d1d..8c67451b86f36e3 100644 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.html +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.html @@ -2,32 +2,40 @@
- - Access Denied - +
-
- You are not authorized to access Monitoring. To use Monitoring, you - need the privileges granted by both the `kibana_user` and - `monitoring_user` roles. -
+
-
- If you are attempting to access a dedicated monitoring cluster, this +
+ i18n-id="xpack.monitoring.accessDenied.backToKibanaButtonLabel" + i18n-default-message="Back to Kibana" + >
diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.html b/x-pack/plugins/monitoring/public/views/alerts/index.html index 115eac165761574..5ef9cc2b0347386 100644 --- a/x-pack/plugins/monitoring/public/views/alerts/index.html +++ b/x-pack/plugins/monitoring/public/views/alerts/index.html @@ -6,13 +6,23 @@
-

Alerts

+

+

diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.js b/x-pack/plugins/monitoring/public/views/alerts/index.js index 1a7e21d32d2eb5e..7b85d19bc8be087 100644 --- a/x-pack/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/plugins/monitoring/public/views/alerts/index.js @@ -45,7 +45,7 @@ uiRoutes.when('/alerts', { }, controllerAs: 'alerts', controller: class AlertsView extends MonitoringViewBaseController { - constructor($injector, $scope) { + constructor($injector, $scope, i18n) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); @@ -53,7 +53,7 @@ uiRoutes.when('/alerts', { $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: globalState.cluster_uuid }); super({ - title: 'Cluster Alerts', + title: i18n('xpack.monitoring.alerts.clusterAlertsTitle', { defaultMessage: 'Cluster Alerts' }), getPageData, $scope, $injector diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.html b/x-pack/plugins/monitoring/public/views/cluster/listing/index.html index 5034260e3dab8d9..5030a67fe3ec57d 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/listing/index.html +++ b/x-pack/plugins/monitoring/public/views/cluster/listing/index.html @@ -1,6 +1,11 @@
-

Clusters

+

+

{ super.renderReact( - + + + ); }; } diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js index 4242ef16405a029..554aaaea3196960 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js @@ -12,6 +12,7 @@ import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; import template from './index.html'; import { MonitoringViewBaseController } from '../../../base_controller'; import { CcrShard } from '../../../../components/elasticsearch/ccr_shard'; +import { I18nProvider } from '@kbn/i18n/react'; uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { template, @@ -24,16 +25,24 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { }, controllerAs: 'elasticsearchCcr', controller: class ElasticsearchCcrController extends MonitoringViewBaseController { - constructor($injector, $scope, pageData) { + constructor($injector, $scope, pageData, i18n) { super({ - title: 'Elasticsearch - Ccr - Shard', + title: i18n('xpack.monitoring.elasticsearch.ccr.shard.routeTitle', { + defaultMessage: 'Elasticsearch - Ccr - Shard' + }), reactNodeId: 'elasticsearchCcrShardReact', getPageData, $scope, $injector }); - $scope.instance = `Index: ${get(pageData, 'stat.follower_index')} Shard: ${get(pageData, 'stat.shard_id')}`; + $scope.instance = i18n('xpack.monitoring.elasticsearch.ccr.shard.instanceTitle', { + defaultMessage: 'Index: {followerIndex} Shard: {shardId}', + values: { + followerIndex: get(pageData, 'stat.follower_index'), + shardId: get(pageData, 'stat.shard_id') + } + }); $scope.$watch(() => this.data, data => { this.renderReact(data); @@ -41,7 +50,7 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { this.renderReact = (props) => { super.renderReact( - + ); }; } diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js index dc353ac8097f154..c011e8540c5f10b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js @@ -46,7 +46,7 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { }, pageData: getPageData }, - controller($injector, $scope) { + controller($injector, $scope, i18n) { timefilter.enableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); @@ -57,7 +57,14 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { $scope.pageData = $route.current.locals.pageData; const title = $injector.get('title'); - title($scope.cluster, `Elasticsearch - Indices - ${$scope.indexName} - Advanced`); + const routeTitle = i18n('xpack.monitoring.elasticsearch.indices.advanced.routeTitle', { + defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', + values: { + indexName: $scope.indexName + } + }); + + title($scope.cluster, routeTitle); const $executor = $injector.get('$executor'); $executor.register({ diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js index 006c622280ea574..12b2e961ceea95a 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js @@ -46,7 +46,7 @@ uiRoutes.when('/elasticsearch/indices/:index', { }, pageData: getPageData }, - controller($injector, $scope) { + controller($injector, $scope, i18n) { timefilter.enableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); @@ -57,7 +57,14 @@ uiRoutes.when('/elasticsearch/indices/:index', { $scope.indexName = $route.current.params.index; const title = $injector.get('title'); - title($scope.cluster, `Elasticsearch - Indices - ${$scope.indexName} - Overview`); + const routeTitle = i18n('xpack.monitoring.elasticsearch.indices.overview.routeTitle', { + defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', + values: { + indexName: $scope.indexName + } + }); + + title($scope.cluster, routeTitle); const $executor = $injector.get('$executor'); $executor.register({ diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js index 07b291dbe020dc7..f8615657500d896 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js @@ -23,7 +23,7 @@ uiRoutes.when('/elasticsearch/indices', { }, controllerAs: 'elasticsearchIndices', controller: class ElasticsearchIndicesController extends MonitoringViewBaseTableController { - constructor($injector, $scope) { + constructor($injector, $scope, i18n) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); const features = $injector.get('features'); @@ -34,7 +34,9 @@ uiRoutes.when('/elasticsearch/indices', { let showSystemIndices = features.isEnabled('showSystemIndices', false); super({ - title: 'Elasticsearch - Indices', + title: i18n('xpack.monitoring.elasticsearch.indices.routeTitle', { + defaultMessage: 'Elasticsearch - Indices' + }), storageKey: 'elasticsearch.indices', apiUrlFn: () => `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices?show_system_indices=${showSystemIndices}`, reactNodeId: 'elasticsearchIndicesReact', diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html index 0e0cb6dbab60c89..61aa9e49317da94 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html @@ -1,7 +1,11 @@
-

Machine Learning Jobs

+

-

Kibana Instances

+

{ - await browser.waitForSelector(`.toast.alert.alert-danger`); - throw new Error('Reporting subject could not be loaded to take a screenshot.'); + const checkForToastMessage = async (browser, layout) => { + await browser.waitForSelector(layout.selectors.toastHeader); + const toastHeaderText = await browser.evaluate({ + fn: function (selector) { + const nodeList = document.querySelectorAll(selector); + return nodeList.item(0).innerText; + }, + args: [layout.selectors.toastHeader], + }); + throw new Error('Encountered an unexpected message on the page: ' + toastHeaderText); }; const getNumberOfItems = async (browser, layout) => { @@ -250,7 +257,6 @@ export function screenshotsObservableFactory(server) { logger.debug(line, ['browserConsole']); }); - const screenshot$ = driver$.pipe( tap(() => logger.debug(`opening ${url}`)), mergeMap( @@ -266,7 +272,7 @@ export function screenshotsObservableFactory(server) { mergeMap( browser => Rx.race( Rx.from(waitForElementOrItemsCountAttribute(browser, layout)), - Rx.from(waitForNotFoundError(browser)) + Rx.from(checkForToastMessage(browser, layout)) ), browser => browser ), diff --git a/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js b/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js index af2036c5382e10b..f8e7f0f254bea9c 100644 --- a/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js +++ b/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import path from 'path'; import { randomBytes } from 'crypto'; import { fromCallback } from 'bluebird'; import { transformFn } from './transform_fn'; @@ -125,7 +124,24 @@ export function PhantomDriver({ page, browser, zoom, logger }) { randomBytes(6).toString('base64'), ].join('-'); - return _injectPromise(page) + const intlPath = require.resolve('intl/dist/Intl.min.js'); + const promisePath = require.resolve('bluebird/js/browser/bluebird.js'); + + return injectPolyfill( + page, + intlPath, + function hasIntl() { + return (window.Intl !== undefined); + } + ) + .then(() => + injectPolyfill( + page, + promisePath, + function hasPromise() { + return (window.Promise !== undefined); + } + )) .then(() => { return fromCallback(cb => { page.evaluate(transformFn(evaluateWrapper), transformFn(fn).toString(), uniqId, args, cb); @@ -315,35 +331,26 @@ export function PhantomDriver({ page, browser, zoom, logger }) { }; } +async function injectPolyfill(page, pathToPolyfillFile, checkFunction) { + const hasPolyfill = await fromCallback(cb => { + page.evaluate(checkFunction, cb); + }); -function _injectPromise(page) { - function checkForPromise() { - return fromCallback(cb => { - page.evaluate(function hasPromise() { - return (typeof window.Promise !== 'undefined'); - }, cb); - }); + if (hasPolyfill) { + return; } - return checkForPromise() - .then(hasPromise => { - if (hasPromise) return; + const status = await fromCallback(cb => page.injectJs(pathToPolyfillFile, cb)); - const nodeModules = path.resolve(__dirname, '..', '..', '..', '..', '..', '..', 'node_modules'); - const promisePath = path.join(nodeModules, 'bluebird', 'js', 'browser', 'bluebird.js'); - return fromCallback(cb => page.injectJs(promisePath, cb)) - .then(status => { - if (status !== true) { - return Promise.reject('Failed to load Promise library'); - } - }) - .then(checkForPromise) - .then(hasPromiseLoaded => { - if (hasPromiseLoaded !== true) { - return Promise.reject('Failed to inject Promise'); - } - }); - }); -} + if (!status) { + return Promise.reject(`Failed to load ${pathToPolyfillFile} library`); + } + const hasPolyfillLoaded = await fromCallback(cb => { + page.evaluate(checkFunction, cb); + }); + if (!hasPolyfillLoaded) { + return Promise.reject(`Failed to inject ${pathToPolyfillFile}`); + } +} diff --git a/x-pack/plugins/security/public/services/role_privileges.js b/x-pack/plugins/security/public/services/role_privileges.js index 794a4b30674e5b0..261e271b91cfb96 100644 --- a/x-pack/plugins/security/public/services/role_privileges.js +++ b/x-pack/plugins/security/public/services/role_privileges.js @@ -17,6 +17,8 @@ const clusterPrivileges = [ 'monitor_ml', 'manage_watcher', 'monitor_watcher', + 'read_ccr', + 'manage_ccr', ]; const indexPrivileges = [ 'all', diff --git a/x-pack/plugins/security/public/services/shield_privileges.js b/x-pack/plugins/security/public/services/shield_privileges.js index a00b326ab82da35..843b5ff318dbea1 100644 --- a/x-pack/plugins/security/public/services/shield_privileges.js +++ b/x-pack/plugins/security/public/services/shield_privileges.js @@ -21,6 +21,8 @@ module.constant('shieldPrivileges', { 'monitor_ml', 'manage_watcher', 'monitor_watcher', + 'read_ccr', + 'manage_ccr', ], indices: [ 'all', diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/collapsible_panel.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/collapsible_panel.test.tsx.snap index 3b3587c7ff524b5..a940a55b3754bc2 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/collapsible_panel.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/collapsible_panel.test.tsx.snap @@ -43,7 +43,11 @@ exports[`it renders without blowing up 1`] = ` onClick={[Function]} type="button" > - hide + diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.test.tsx index 86f1e73b78e1b90..b1e23cb43983ede 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.test.tsx @@ -5,12 +5,12 @@ */ import { EuiLink } from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { CollapsiblePanel } from './collapsible_panel'; test('it renders without blowing up', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl(

child

@@ -20,7 +20,7 @@ test('it renders without blowing up', () => { }); test('it renders children by default', () => { - const wrapper = mount( + const wrapper = mountWithIntl(

child 1

child 2

@@ -32,7 +32,7 @@ test('it renders children by default', () => { }); test('it hides children when the "hide" link is clicked', () => { - const wrapper = mount( + const wrapper = mountWithIntl(

child 1

child 2

diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.tsx index f8271d4d83d2a74..1a2907b790c9c52 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; interface Props { @@ -55,7 +56,19 @@ export class CollapsiblePanel extends Component { - {this.state.collapsed ? 'show' : 'hide'} + + {this.state.collapsed ? ( + + ) : ( + + )} + ); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx index 8a78748b4623232..cc16866c8835558 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx @@ -9,20 +9,20 @@ import { // @ts-ignore EuiConfirmModal, } from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DeleteRoleButton } from './delete_role_button'; test('it renders without crashing', () => { const deleteHandler = jest.fn(); - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper.find(EuiButtonEmpty)).toHaveLength(1); expect(deleteHandler).toHaveBeenCalledTimes(0); }); test('it shows a confirmation dialog when clicked', () => { const deleteHandler = jest.fn(); - const wrapper = mount(); + const wrapper = mountWithIntl(); wrapper.find(EuiButtonEmpty).simulate('click'); @@ -33,7 +33,7 @@ test('it shows a confirmation dialog when clicked', () => { test('it renders nothing when canDelete is false', () => { const deleteHandler = jest.fn(); - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper.find('*')).toHaveLength(0); expect(deleteHandler).toHaveBeenCalledTimes(0); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx index 28b3107a96c42f2..22820edd73c9874 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx @@ -11,6 +11,7 @@ import { // @ts-ignore EuiOverlayMask, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; interface Props { @@ -35,7 +36,10 @@ export class DeleteRoleButton extends Component { return ( - Delete role + {this.maybeShowModal()} @@ -49,15 +53,40 @@ export class DeleteRoleButton extends Component { return ( + } onCancel={this.closeModal} onConfirm={this.onConfirmDelete} - cancelButtonText={"No, don't delete"} - confirmButtonText={'Yes, delete role'} + cancelButtonText={ + + } + confirmButtonText={ + + } buttonColor={'danger'} > -

Are you sure you want to delete this role?

-

This action cannot be undone!

+

+ +

+

+ +

); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx index 98f8bbcf1087348..a3b6b58d09d06c3 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx @@ -3,6 +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 { EuiButton, EuiButtonEmpty, @@ -19,6 +20,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; import { toastNotifications } from 'ui/notify'; @@ -47,6 +49,7 @@ interface Props { spaces?: Space[]; spacesEnabled: boolean; userProfile: UserProfile; + intl: InjectedIntl; } interface State { @@ -54,7 +57,7 @@ interface State { formError: RoleValidationResult | null; } -export class EditRolePage extends Component { +class EditRolePageUI extends Component { private validator: RoleValidator; constructor(props: Props) { @@ -67,9 +70,17 @@ export class EditRolePage extends Component { } public render() { - const description = this.props.spacesEnabled - ? `Set privileges on your Elasticsearch data and control access to your Kibana spaces.` - : `Set privileges on your Elasticsearch data and control access to Kibana.`; + const description = this.props.spacesEnabled ? ( + + ) : ( + + ); return ( @@ -86,7 +97,10 @@ export class EditRolePage extends Component {

- Reserved roles are built-in and cannot be removed or modified. +

@@ -115,12 +129,27 @@ export class EditRolePage extends Component { tabIndex: 0, }; if (isReservedRole(this.props.role)) { - titleText = 'Viewing role'; + titleText = ( + + ); props['aria-describedby'] = 'reservedRoleDescription'; } else if (this.editingExistingRole()) { - titleText = 'Edit role'; + titleText = ( + + ); } else { - titleText = 'Create role'; + titleText = ( + + ); } return ( @@ -148,11 +177,21 @@ export class EditRolePage extends Component { return ( + } helpText={ - !isReservedRole(this.props.role) && this.editingExistingRole() - ? "A role's name cannot be changed once it has been created." - : undefined + !isReservedRole(this.props.role) && this.editingExistingRole() ? ( + + ) : ( + undefined + ) } {...this.validator.validateRoleName(this.state.role)} > @@ -225,10 +264,27 @@ export class EditRolePage extends Component { public getFormButtons = () => { if (isReservedRole(this.props.role)) { - return Return to role list; + return ( + + + + ); } - const saveText = this.editingExistingRole() ? 'Update role' : 'Create role'; + const saveText = this.editingExistingRole() ? ( + + ) : ( + + ); return ( @@ -244,7 +300,10 @@ export class EditRolePage extends Component { - Cancel + @@ -274,7 +333,7 @@ export class EditRolePage extends Component { formError: null, }); - const { httpClient } = this.props; + const { httpClient, intl } = this.props; const role = { ...this.state.role, @@ -287,7 +346,12 @@ export class EditRolePage extends Component { saveRole(httpClient, role) .then(() => { - toastNotifications.addSuccess('Saved role'); + toastNotifications.addSuccess( + intl.formatMessage({ + id: 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', + defaultMessage: 'Saved role', + }) + ); this.backToRoleList(); }) .catch((error: any) => { @@ -297,11 +361,16 @@ export class EditRolePage extends Component { }; public handleDeleteRole = () => { - const { httpClient, role } = this.props; + const { httpClient, role, intl } = this.props; deleteRole(httpClient, role.name) .then(() => { - toastNotifications.addSuccess('Deleted role'); + toastNotifications.addSuccess( + intl.formatMessage({ + id: 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', + defaultMessage: 'Deleted role', + }) + ); this.backToRoleList(); }) .catch((error: any) => { @@ -313,3 +382,5 @@ export class EditRolePage extends Component { window.location.hash = ROLES_PATH; }; } + +export const EditRolePage = injectI18n(EditRolePageUI); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap index 2acd8fa18e63b81..488f8a49c866102 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap @@ -71,6 +71,14 @@ exports[`it renders without crashing 1`] = ` "isGroupLabelOption": false, "label": "monitor_watcher", }, + Object { + "isGroupLabelOption": false, + "label": "read_ccr", + }, + Object { + "isGroupLabelOption": false, + "label": "manage_ccr", + }, ] } selectedOptions={Array []} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 416cec99b327137..02d44c23ac5c36c 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -1,181 +1,220 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`it renders without crashing 1`] = ` - - - - Manage the actions this role can perform against your cluster. - - - Learn more - -

- } - fullWidth={false} - gutterSize="l" - title={ + + + + + + + + +

+ } + fullWidth={false} + gutterSize="l" + title={ +

+ +

+ } + titleSize="xs" + > + + + +
+ + + + + + +

+ } + fullWidth={false} + gutterSize="l" + title={ +

+ +

+ } + titleSize="xs" + > + + + +
+ +

- Cluster privileges +

- } - titleSize="xs" - > - + + - - -
- - - Allow requests to be submitted on the behalf of other users. - + - Learn more +

- } - fullWidth={false} - gutterSize="l" - title={ -

- Run As privileges -

- } - titleSize="xs" - > - - - -
- - -

- Index privileges -

-
- - -

- Control access to the data in your cluster. - - - Learn more - -

-
- + - - - Add index privilege - -
-
+ /> + + + + + +
+ `; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap index c55876b87186b44..54446baa76a6f71 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap @@ -39,7 +39,13 @@ exports[`it renders without crashing 1`] = ` fullWidth={true} hasEmptyLabelSpace={false} isInvalid={false} - label="Indices" + label={ + + } > + } > + } + label={ + + } > + } onChange={[Function]} value={false} /> diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx index 3a948801102a570..b58cab2197fe428 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { RoleValidator } from '../../../lib/validate_role'; import { ClusterPrivileges } from './cluster_privileges'; import { ElasticsearchPrivileges } from './elasticsearch_privileges'; @@ -34,7 +34,9 @@ test('it renders without crashing', () => { allowFieldLevelSecurity: true, validator: new RoleValidator(), }; - const wrapper = shallow(); + const wrapper = shallowWithIntl( + + ); expect(wrapper).toMatchSnapshot(); }); @@ -61,7 +63,9 @@ test('it renders ClusterPrivileges', () => { allowFieldLevelSecurity: true, validator: new RoleValidator(), }; - const wrapper = mount(); + const wrapper = mountWithIntl( + + ); expect(wrapper.find(ClusterPrivileges)).toHaveLength(1); }); @@ -88,6 +92,8 @@ test('it renders IndexPrivileges', () => { allowFieldLevelSecurity: true, validator: new RoleValidator(), }; - const wrapper = mount(); + const wrapper = mountWithIntl( + + ); expect(wrapper.find(IndexPrivileges)).toHaveLength(1); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx index 2625ff9879d3fbe..d2caf2405381580 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx @@ -16,6 +16,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage, I18nProvider, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { Role } from '../../../../../../../common/model/role'; // @ts-ignore @@ -36,14 +37,17 @@ interface Props { indexPatterns: string[]; allowDocumentLevelSecurity: boolean; allowFieldLevelSecurity: boolean; + intl: InjectedIntl; } -export class ElasticsearchPrivileges extends Component { +class ElasticsearchPrivilegesUI extends Component { public render() { return ( - - {this.getForm()} - + + + {this.getForm()} + + ); } @@ -56,6 +60,7 @@ export class ElasticsearchPrivileges extends Component { indexPatterns, allowDocumentLevelSecurity, allowFieldLevelSecurity, + intl, } = this.props; const indexProps = { @@ -71,10 +76,20 @@ export class ElasticsearchPrivileges extends Component { return ( Cluster privileges} + title={ +

+ +

+ } description={

- Manage the actions this role can perform against your cluster.{' '} + {this.learnMore(documentationLinks.esClusterPrivileges)}

} @@ -87,17 +102,35 @@ export class ElasticsearchPrivileges extends Component { Run As privileges} + title={ +

+ +

+ } description={

- Allow requests to be submitted on the behalf of other users.{' '} + {this.learnMore(documentationLinks.esRunAsPrivileges)}

} > ({ id: username, label: username, @@ -113,12 +146,20 @@ export class ElasticsearchPrivileges extends Component { -

Index privileges

+

+ +

- Control access to the data in your cluster.{' '} + {this.learnMore(documentationLinks.esIndicesPrivileges)}

@@ -129,7 +170,10 @@ export class ElasticsearchPrivileges extends Component { {this.props.editable && ( - Add index privilege + )}
@@ -138,7 +182,10 @@ export class ElasticsearchPrivileges extends Component { public learnMore = (href: string) => ( - Learn more + ); @@ -189,3 +236,5 @@ export class ElasticsearchPrivileges extends Component { this.props.onChange(role); }; } + +export const ElasticsearchPrivileges = injectI18n(ElasticsearchPrivilegesUI); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx index 2d6867669430448..dd3092293d3bcc3 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiButtonIcon, EuiSwitch, EuiTextArea } from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { RoleValidator } from '../../../lib/validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; @@ -31,7 +31,7 @@ test('it renders without crashing', () => { onDelete: jest.fn(), }; - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); @@ -62,7 +62,7 @@ describe('delete button', () => { ...props, allowDelete: false, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); }); @@ -71,7 +71,7 @@ describe('delete button', () => { ...props, allowDelete: true, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find(EuiButtonIcon)).toHaveLength(1); }); @@ -80,7 +80,7 @@ describe('delete button', () => { ...props, allowDelete: true, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); wrapper.find(EuiButtonIcon).simulate('click'); expect(testProps.onDelete).toHaveBeenCalledTimes(1); }); @@ -114,7 +114,7 @@ describe(`document level security`, () => { allowDocumentLevelSecurity: false, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find(EuiSwitch)).toHaveLength(0); expect(wrapper.find(EuiTextArea)).toHaveLength(0); }); @@ -128,7 +128,7 @@ describe(`document level security`, () => { }, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find(EuiSwitch)).toHaveLength(1); expect(wrapper.find(EuiTextArea)).toHaveLength(0); }); @@ -138,7 +138,7 @@ describe(`document level security`, () => { ...props, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find(EuiSwitch)).toHaveLength(1); expect(wrapper.find(EuiTextArea)).toHaveLength(1); }); @@ -172,7 +172,7 @@ describe('field level security', () => { allowFieldLevelSecurity: false, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find('.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0); }); @@ -181,7 +181,7 @@ describe('field level security', () => { ...props, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); }); @@ -196,7 +196,7 @@ describe('field level security', () => { }, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); expect(wrapper.find('.euiFormHelpText')).toHaveLength(1); }); @@ -206,7 +206,7 @@ describe('field level security', () => { ...props, }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); expect(wrapper.find('.euiFormHelpText')).toHaveLength(0); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx index 6f1416ab43552ee..42834afa888ee45 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx @@ -15,6 +15,7 @@ import { EuiSwitch, EuiTextArea, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; import { IndexPrivilege } from '../../../../../../../common/model/index_privilege'; // @ts-ignore @@ -81,7 +82,12 @@ export class IndexPrivilegeForm extends Component { + } fullWidth={true} {...this.props.validator.validateIndexPrivilege(this.props.indexPrivilege)} > @@ -96,7 +102,15 @@ export class IndexPrivilegeForm extends Component { - + + } + fullWidth={true} + > { return ( + } fullWidth={true} className="indexPrivilegeForm__grantedFieldsRow" helpText={ - !isReservedRole && grant.length === 0 - ? 'If no fields are granted, then users assigned to this role will not be able to see any data for this index.' - : undefined + !isReservedRole && grant.length === 0 ? ( + + ) : ( + undefined + ) } > @@ -170,7 +194,12 @@ export class IndexPrivilegeForm extends Component { + } // @ts-ignore compressed={true} // @ts-ignore @@ -181,7 +210,15 @@ export class IndexPrivilegeForm extends Component { )} {this.state.queryExpanded && ( - + + } + fullWidth={true} + > { allowFieldLevelSecurity: true, validator: new RoleValidator(), }; - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); @@ -65,6 +65,6 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', () => { allowFieldLevelSecurity: true, validator: new RoleValidator(), }; - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find(IndexPrivilegeForm)).toHaveLength(1); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/impacted_spaces_flyout.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/impacted_spaces_flyout.test.tsx.snap index c67cde88bb44472..c749a49bce1102a 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/impacted_spaces_flyout.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/impacted_spaces_flyout.test.tsx.snap @@ -10,7 +10,11 @@ exports[` renders without crashing 1`] = ` onClick={[Function]} type="button" > - View summary of spaces privileges +
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap index 50ef26bbe56accb..82d0190ad54cc80 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap @@ -1,56 +1,58 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders without crashing 1`] = ` - - + + - + /> + + `; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_callout_warning.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_callout_warning.test.tsx.snap index 53f3fc716d65e85..bdce226863ef3c0 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_callout_warning.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_callout_warning.test.tsx.snap @@ -3,13 +3,101 @@ exports[`PrivilegeCalloutWarning renders without crashing 1`] = ` + } >
- Minimum privilege is too high to customize individual spaces + + Minimum privilege is too high to customize individual spaces +

- Setting the minimum privilege to - - all - - grants full access to all spaces. To customize privileges for individual spaces, the minimum privilege must be either - - read - - or - - none - - . + + + , + "noneText": + + , + "readText": + + , + } + } + > + Setting the minimum privilege to + + + all + + + grants full access to all + spaces. To customize privileges for individual spaces, the minimum privilege must be + either + + + read + + + or + + + none + + + . +

diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap index c24b2d596ff2219..a435e7c43ceb131 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap @@ -19,7 +19,13 @@ exports[` renders without crashing 1`] = ` fullWidth={false} hasEmptyLabelSpace={false} isInvalid={false} - label="Spaces" + label={ + + } > renders without crashing 1`] = ` fullWidth={false} hasEmptyLabelSpace={false} isInvalid={false} - label="Privilege" + label={ + + } > renders without crashing 1`] = ` - Specifies the Kibana privilege for this role. +

} fullWidth={false} gutterSize="l" title={

- Kibana privileges +

} titleSize="xs" diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap index c032aec950064b3..da21290b11e24b0 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` hides the space table if there are no existing space privileges 1`] = ` - hides the space table if there are no existin }, ] } -/> +> + + `; exports[` renders without crashing 1`] = ` @@ -47,14 +169,22 @@ exports[` renders without crashing 1`] = ` - Specify the minimum actions users can perform in your spaces. +

} fullWidth={false} gutterSize="l" title={

- Minimum privileges for all spaces +

} titleSize="xs" @@ -89,7 +219,11 @@ exports[` renders without crashing 1`] = ` textTransform="none" >

- Higher privileges for individual spaces +

renders without crashing 1`] = ` size="s" >

- Grant more privileges on a per space basis. For example, if the privileges are - - - read - - for all spaces, you can set the privileges to - - all - - - for an individual space. + + + , + "read": + + , + } + } + />

- renders without crashing 1`] = ` size="s" type="button" > - Add space privilege + - with user profile disabling "manageSpaces" re size="m" title={

- Insufficient Privileges +

} >

- You are not authorized to view all available spaces. +

- Please ensure your account has all privileges granted by the - - - kibana_user - - role, and try again. + + + , + } + } + />

`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx index aafe9d273c5c4a5..8a103ff493165d3 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx @@ -5,8 +5,8 @@ */ import { EuiFlyout, EuiLink } from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ImpactedSpacesFlyout } from './impacted_spaces_flyout'; import { PrivilegeSpaceTable } from './privilege_space_table'; @@ -52,16 +52,24 @@ const buildProps = (customProps = {}) => { describe('', () => { it('renders without crashing', () => { - expect(shallow()).toMatchSnapshot(); + expect( + shallowWithIntl( + + ) + ).toMatchSnapshot(); }); it('does not immediately show the flyout', () => { - const wrapper = mount(); + const wrapper = mountWithIntl( + + ); expect(wrapper.find(EuiFlyout)).toHaveLength(0); }); it('shows the flyout after clicking the link', () => { - const wrapper = mount(); + const wrapper = mountWithIntl( + + ); wrapper.find(EuiLink).simulate('click'); expect(wrapper.find(EuiFlyout)).toHaveLength(1); }); @@ -82,7 +90,9 @@ describe('', () => { }, }); - const wrapper = shallow(); + const wrapper = shallowWithIntl( + + ); wrapper.find(EuiLink).simulate('click'); const table = wrapper.find(PrivilegeSpaceTable); @@ -112,7 +122,9 @@ describe('', () => { }, }); - const wrapper = shallow(); + const wrapper = shallowWithIntl( + + ); wrapper.find(EuiLink).simulate('click'); const table = wrapper.find(PrivilegeSpaceTable); @@ -141,7 +153,9 @@ describe('', () => { }, }); - const wrapper = shallow(); + const wrapper = shallowWithIntl( + + ); wrapper.find(EuiLink).simulate('click'); const table = wrapper.find(PrivilegeSpaceTable); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx index 1636bf14a484a18..f811912e6148f28 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx @@ -12,6 +12,7 @@ import { EuiLink, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { PrivilegeSpaceTable } from './privilege_space_table'; @@ -26,13 +27,14 @@ interface Props { role: Role; spaces: Space[]; userProfile: UserProfile; + intl: InjectedIntl; } interface State { showImpactedSpaces: boolean; } -export class ImpactedSpacesFlyout extends Component { +class ImpactedSpacesFlyoutUI extends Component { constructor(props: Props) { super(props); this.state = { @@ -46,7 +48,10 @@ export class ImpactedSpacesFlyout extends Component {
- View summary of spaces privileges +
{flyout} @@ -61,13 +66,23 @@ export class ImpactedSpacesFlyout extends Component { }; public getHighestPrivilege(...privileges: KibanaPrivilege[]): KibanaPrivilege { + const { intl } = this.props; if (privileges.indexOf('all') >= 0) { - return 'all'; + return intl.formatMessage({ + id: 'xpack.security.management.editRoles.impactedSpacesFlyout.allLabel', + defaultMessage: 'all', + }) as KibanaPrivilege; } if (privileges.indexOf('read') >= 0) { - return 'read'; + return intl.formatMessage({ + id: 'xpack.security.management.editRoles.impactedSpacesFlyout.readLabel', + defaultMessage: 'read', + }) as KibanaPrivilege; } - return 'none'; + return intl.formatMessage({ + id: 'xpack.security.management.editRoles.impactedSpacesFlyout.noneLabel', + defaultMessage: 'none', + }) as KibanaPrivilege; } public getFlyout = () => { @@ -106,7 +121,12 @@ export class ImpactedSpacesFlyout extends Component { > -

Summary of space privileges

+

+ +

@@ -125,3 +145,5 @@ export class ImpactedSpacesFlyout extends Component { ); }; } + +export const ImpactedSpacesFlyout = injectI18n(ImpactedSpacesFlyoutUI); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx index 80d848b5893d519..12783ecd150bfc6 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { I18nProvider } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { Space } from '../../../../../../../../spaces/common/model/space'; import { UserProfile } from '../../../../../../../../xpack_main/public/services/user_profile'; @@ -28,9 +29,11 @@ interface Props { export class KibanaPrivileges extends Component { public render() { return ( - - {this.getForm()} - + + + {this.getForm()} + + ); } diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.test.tsx index 1e8d3f3c3915847..4ee2640ed1c9e4d 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { PrivilegeCalloutWarning } from './privilege_callout_warning'; describe('PrivilegeCalloutWarning', () => { it('renders without crashing', () => { expect( - mount() + mountWithIntl() ).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx index 0b5666f39157341..ed24e61e5f1fd77 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; import { NO_PRIVILEGE_VALUE } from '../../../lib/constants'; @@ -33,11 +34,19 @@ export class PrivilegeCalloutWarning extends Component { + } >

- This role always grants full access to all spaces. To customize privileges for - individual spaces, you must create a new role. +

); @@ -46,12 +55,46 @@ export class PrivilegeCalloutWarning extends Component { + } >

- Setting the minimum privilege to all grants full access to all - spaces. To customize privileges for individual spaces, the minimum privilege must be - either read or none. + + + + ), + readText: ( + + + + ), + noneText: ( + + + + ), + }} + />

); @@ -64,11 +107,19 @@ export class PrivilegeCalloutWarning extends Component { + } >

- This role always grants read access to all spaces. To customize privileges for - individual spaces, you must create a new role. +

); @@ -79,7 +130,20 @@ export class PrivilegeCalloutWarning extends Component { iconType="iInCircle" title={ - The minimal possible privilege is read. + + + + ), + }} + /> } /> @@ -92,11 +156,19 @@ export class PrivilegeCalloutWarning extends Component { + } >

- This role never grants access to any spaces within Kibana. To customize privileges for - individual spaces, you must create a new role. +

); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.test.tsx index 95494567446b74a..800c628b17243a0 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; import { RoleValidator } from '../../../lib/validate_role'; import { PrivilegeSpaceForm } from './privilege_space_form'; @@ -40,6 +40,6 @@ const buildProps = (customProps = {}) => { describe('', () => { it('renders without crashing', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx index cf7bcf8287b9d6c..4907a5b41a53cf6 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx @@ -5,6 +5,7 @@ */ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { Space } from '../../../../../../../../spaces/common/model/space'; import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; @@ -41,7 +42,12 @@ export class PrivilegeSpaceForm extends Component { + } {...validator.validateSelectedSpaces(selectedSpaceIds, selectedPrivilege)} > { + } {...validator.validateSelectedPrivilege(selectedSpaceIds, selectedPrivilege)} > void; readonly?: boolean; + intl: InjectedIntl; } interface State { @@ -35,13 +37,13 @@ interface DeletedSpace extends Space { deleted: boolean; } -export class PrivilegeSpaceTable extends Component { +class PrivilegeSpaceTableUI extends Component { public state = { searchTerm: '', }; public render() { - const { role, spaces, availablePrivileges, spacePrivileges } = this.props; + const { role, spaces, availablePrivileges, spacePrivileges, intl } = this.props; const { searchTerm } = this.state; @@ -74,7 +76,10 @@ export class PrivilegeSpaceTable extends Component { search={{ box: { incremental: true, - placeholder: 'Filter', + placeholder: intl.formatMessage({ + id: 'xpack.security.management.editRoles.privilegeSpaceTable.filterPlaceholder', + defaultMessage: 'Filter', + }), }, onChange: (search: any) => { this.setState({ @@ -88,6 +93,7 @@ export class PrivilegeSpaceTable extends Component { } public getTableColumns = (role: Role, availablePrivileges: KibanaPrivilege[] = []) => { + const { intl } = this.props; const columns: any[] = [ { field: 'space', @@ -103,11 +109,22 @@ export class PrivilegeSpaceTable extends Component { }, { field: 'space', - name: 'Space', + name: intl.formatMessage({ + id: 'xpack.security.management.editRoles.privilegeSpaceTable.spaceName', + defaultMessage: 'Space', + }), width: this.props.readonly ? '75%' : '50%', render: (space: Space | DeletedSpace) => { if ('deleted' in space) { - return {space.id} (deleted); + return ( + + + + ); } else { return {space.name}; } @@ -115,7 +132,10 @@ export class PrivilegeSpaceTable extends Component { }, { field: 'privilege', - name: 'Privilege', + name: intl.formatMessage({ + id: 'xpack.security.management.editRoles.privilegeSpaceTable.privilegeName', + defaultMessage: 'Privilege', + }), width: this.props.readonly ? '25%' : undefined, render: (privilege: KibanaPrivilege, record: any) => { if (this.props.readonly || record.space.deleted) { @@ -137,7 +157,10 @@ export class PrivilegeSpaceTable extends Component { ]; if (!this.props.readonly) { columns.push({ - name: 'Actions', + name: intl.formatMessage({ + id: 'xpack.security.management.editRoles.privilegeSpaceTable.actionsName', + defaultMessage: 'Actions', + }), actions: [ { render: (record: any) => { @@ -182,3 +205,5 @@ export class PrivilegeSpaceTable extends Component { } }; } + +export const PrivilegeSpaceTable = injectI18n(PrivilegeSpaceTableUI); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx index d6ecbdb705b7460..00221e2cd144f64 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { PrivilegeSelector } from './privilege_selector'; import { SimplePrivilegeForm } from './simple_privilege_form'; @@ -32,12 +32,12 @@ const buildProps = (customProps?: any) => { describe('', () => { it('renders without crashing', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); it('displays "none" when no privilege is selected', () => { const props = buildProps(); - const wrapper = shallow(); + const wrapper = shallowWithIntl(); const selector = wrapper.find(PrivilegeSelector); expect(selector.props()).toMatchObject({ value: 'none', @@ -53,7 +53,7 @@ describe('', () => { }, }, }); - const wrapper = shallow(); + const wrapper = shallowWithIntl(); const selector = wrapper.find(PrivilegeSelector); expect(selector.props()).toMatchObject({ value: 'read', @@ -62,7 +62,7 @@ describe('', () => { it('fires its onChange callback when the privilege changes', () => { const props = buildProps(); - const wrapper = mount(); + const wrapper = mountWithIntl(); const selector = wrapper.find(PrivilegeSelector).find('select'); selector.simulate('change', { target: { value: 'all' } }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx index 71b881c4a85efe8..ceec0d212488994 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx @@ -9,6 +9,7 @@ import { EuiDescribedFormGroup, EuiFormRow, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; import { Role } from '../../../../../../../common/model/role'; @@ -35,11 +36,28 @@ export class SimplePrivilegeForm extends Component { ? (assignedPrivileges.global[0] as KibanaPrivilege) : NO_PRIVILEGE_VALUE; - const description =

Specifies the Kibana privilege for this role.

; + const description = ( +

+ +

+ ); return ( - Kibana privileges} description={description}> + + + + } + description={description} + > { describe('', () => { it('renders without crashing', () => { - expect(shallow()).toMatchSnapshot(); + expect( + shallowWithIntl() + ).toMatchSnapshot(); }); it('shows the space table if exisitng space privileges are declared', () => { @@ -66,7 +68,7 @@ describe('', () => { }, }); - const wrapper = mount(); + const wrapper = mountWithIntl(); const table = wrapper.find(PrivilegeSpaceTable); expect(table).toHaveLength(1); @@ -75,7 +77,7 @@ describe('', () => { it('hides the space table if there are no existing space privileges', () => { const props = buildProps(); - const wrapper = mount(); + const wrapper = mountWithIntl(); const table = wrapper.find(PrivilegeSpaceTable); expect(table).toMatchSnapshot(); @@ -96,7 +98,7 @@ describe('', () => { }, }); - const wrapper = mount(); + const wrapper = mountWithIntl(); expect(wrapper.find(PrivilegeSpaceForm)).toHaveLength(0); wrapper.find('button[data-test-subj="addSpacePrivilegeButton"]').simulate('click'); @@ -120,7 +122,7 @@ describe('', () => { }, }); - const wrapper = mount(); + const wrapper = mountWithIntl(); const warning = wrapper.find(PrivilegeCalloutWarning); expect(warning.props()).toMatchObject({ @@ -151,7 +153,7 @@ describe('', () => { }, }); - const wrapper = mount(); + const wrapper = mountWithIntl(); const warning = wrapper.find(PrivilegeCalloutWarning); expect(warning.props()).toMatchObject({ @@ -174,7 +176,7 @@ describe('', () => { }, }); - const wrapper = mount(); + const wrapper = mountWithIntl(); const table = wrapper.find(PrivilegeSpaceTable); expect(table).toHaveLength(1); @@ -200,7 +202,7 @@ describe('', () => { }, }); - const wrapper = mount(); + const wrapper = mountWithIntl(); const warning = wrapper.find(PrivilegeCalloutWarning); expect(warning).toHaveLength(0); @@ -221,7 +223,7 @@ describe('', () => { }, }); - const wrapper = mount(); + const wrapper = mountWithIntl(); const table = wrapper.find(PrivilegeSpaceTable); expect(table).toHaveLength(1); @@ -244,7 +246,7 @@ describe('', () => { }, }); - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx index 6928d46565e66dc..cd4bdb2f41b44bc 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx @@ -16,6 +16,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { Space } from '../../../../../../../../spaces/common/model/space'; import { UserProfile } from '../../../../../../../../xpack_main/public/services/user_profile'; @@ -40,6 +41,7 @@ interface Props { editable: boolean; validator: RoleValidator; userProfile: UserProfile; + intl: InjectedIntl; } interface PrivilegeForm { @@ -56,7 +58,7 @@ interface State { privilegeForms: PrivilegeForm[]; } -export class SpaceAwarePrivilegeForm extends Component { +class SpaceAwarePrivilegeFormUI extends Component { constructor(props: Props) { super(props); const { role } = props; @@ -73,15 +75,44 @@ export class SpaceAwarePrivilegeForm extends Component { } public render() { - const { kibanaAppPrivileges, role, userProfile } = this.props; + const { kibanaAppPrivileges, role, userProfile, intl } = this.props; if (!userProfile.hasCapability('manageSpaces')) { return ( - Insufficient Privileges

} iconType="alert" color="danger"> -

You are not authorized to view all available spaces.

+ + +

+ } + iconType="alert" + color="danger" + >

- Please ensure your account has all privileges granted by the{' '} - kibana_user role, and try again. + +

+

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

); @@ -92,21 +123,46 @@ export class SpaceAwarePrivilegeForm extends Component { const basePrivilege = assignedPrivileges.global.length > 0 ? assignedPrivileges.global[0] : NO_PRIVILEGE_VALUE; - const description =

Specify the minimum actions users can perform in your spaces.

; + const description = ( +

+ +

+ ); let helptext; if (basePrivilege === NO_PRIVILEGE_VALUE) { - helptext = 'No access to spaces'; + helptext = intl.formatMessage({ + id: 'xpack.security.management.editRoles.spaceAwarePrivilegeForm.noAccessToSpacesHelpText', + defaultMessage: 'No access to spaces', + }); } else if (basePrivilege === 'all') { - helptext = 'View, edit, and share objects and apps within all spaces'; + helptext = intl.formatMessage({ + id: + 'xpack.security.management.editRoles.spaceAwarePrivilegeForm.viewEditShareAppsWithinAllSpacesHelpText', + defaultMessage: 'View, edit, and share objects and apps within all spaces', + }); } else if (basePrivilege === 'read') { - helptext = 'View objects and apps within all spaces'; + helptext = intl.formatMessage({ + id: + 'xpack.security.management.editRoles.spaceAwarePrivilegeForm.viewObjectsAndAppsWithinAllSpacesHelpText', + defaultMessage: 'View objects and apps within all spaces', + }); } return ( Minimum privileges for all spaces} + title={ +

+ +

+ } description={description} > @@ -147,7 +203,12 @@ export class SpaceAwarePrivilegeForm extends Component { return ( -

Higher privileges for individual spaces

+

+ +

{ color={'subdued'} >

- Grant more privileges on a per space basis. For example, if the privileges are{' '} - read for all spaces, you can set the privileges to all{' '} - for an individual space. + + + + ), + all: ( + + + + ), + }} + />

@@ -200,7 +282,10 @@ export class SpaceAwarePrivilegeForm extends Component { iconType={'plusInCircle'} onClick={this.addSpacePrivilege} > - Add space privilege +
)} @@ -367,3 +452,5 @@ export class SpaceAwarePrivilegeForm extends Component { this.props.onChange(role); }; } + +export const SpaceAwarePrivilegeForm = injectI18n(SpaceAwarePrivilegeFormUI); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx index 2966c78d2d92aaf..77f6bc85cf07398 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Role } from '../../../../../common/model/role'; import { isReservedRole } from '../../../../lib/role'; @@ -19,7 +20,14 @@ export const ReservedRoleBadge = (props: Props) => { if (isReservedRole(role)) { return ( - + + } + > ); diff --git a/x-pack/plugins/security/public/views/management/edit_role/index.js b/x-pack/plugins/security/public/views/management/edit_role/index.js index 4d2e83a6f8072cd..2377542834ed7aa 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/plugins/security/public/views/management/edit_role/index.js @@ -28,6 +28,7 @@ import { EditRolePage } from './components'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { KibanaAppPrivileges } from '../../../../common/model/kibana_privilege'; +import { I18nProvider } from '@kbn/i18n/react'; routes.when(`${EDIT_ROLES_PATH}/:name?`, { template, @@ -126,20 +127,23 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, { $scope.$$postDigest(() => { const domNode = document.getElementById('editRoleReactRoot'); - render(, domNode); + render( + + + , domNode); // unmount react on controller destroy $scope.$on('$destroy', () => { diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap b/x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap index 1f4837b07d0b902..faf1f4704ce0d5f 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`validateIndexPrivileges it throws when indices is not an array 1`] = `"Expected role.elasticsearch.indices to be an array"`; +exports[`validateIndexPrivileges it throws when indices is not an array 1`] = `"Expected \\"role.elasticsearch.indices\\" to be an array"`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.ts b/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.ts index f4ada14fde8efaa..133d901b6e16636 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.ts +++ b/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.ts @@ -10,6 +10,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { IndexPrivilege } from '../../../../../common/model/index_privilege'; import { KibanaPrivilege } from '../../../../../common/model/kibana_privilege'; import { Role } from '../../../../../common/model/role'; @@ -52,14 +53,34 @@ export class RoleValidator { } if (!role.name) { - return invalid(`Please provide a role name`); + return invalid( + i18n.translate( + 'xpack.security.management.editRoles.validateRole.provideRoleNameWarningMessage', + { + defaultMessage: 'Please provide a role name', + } + ) + ); } if (role.name.length > 1024) { - return invalid(`Name must not exceed 1024 characters`); + return invalid( + i18n.translate( + 'xpack.security.management.editRoles.validateRole.nameLengthWarningMessage', + { + defaultMessage: 'Name must not exceed 1024 characters', + } + ) + ); } if (!role.name.match(/^[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*$/)) { return invalid( - `Name must begin with a letter or underscore and contain only letters, underscores, and numbers.` + i18n.translate( + 'xpack.security.management.editRoles.validateRole.nameAllowedCharactersWarningMessage', + { + defaultMessage: + 'Name must begin with a letter or underscore and contain only letters, underscores, and numbers.', + } + ) ); } return valid(); @@ -71,7 +92,14 @@ export class RoleValidator { } if (!Array.isArray(role.elasticsearch.indices)) { - throw new TypeError(`Expected role.elasticsearch.indices to be an array`); + throw new TypeError( + i18n.translate('xpack.security.management.editRoles.validateRole.indicesTypeErrorMessage', { + defaultMessage: 'Expected {elasticIndices} to be an array', + values: { + elasticIndices: '"role.elasticsearch.indices"', + }, + }) + ); } const areIndicesValid = @@ -91,7 +119,14 @@ export class RoleValidator { } if (indexPrivilege.names.length && !indexPrivilege.privileges.length) { - return invalid(`At least one privilege is required`); + return invalid( + i18n.translate( + 'xpack.security.management.editRoles.validateRole.onePrivilegeRequiredWarningMessage', + { + defaultMessage: 'At least one privilege is required', + } + ) + ); } return valid(); } @@ -112,7 +147,14 @@ export class RoleValidator { if (Array.isArray(spaceIds) && spaceIds.length > 0) { return valid(); } - return invalid('At least one space is required'); + return invalid( + i18n.translate( + 'xpack.security.management.editRoles.validateRole.oneSpaceRequiredWarningMessage', + { + defaultMessage: 'At least one space is required', + } + ) + ); } public validateSelectedPrivilege( @@ -131,7 +173,14 @@ export class RoleValidator { if (privilege) { return valid(); } - return invalid('Privilege is required'); + return invalid( + i18n.translate( + 'xpack.security.management.editRoles.validateRole.privilegeRequiredWarningMessage', + { + defaultMessage: 'Privilege is required', + } + ) + ); } public setInProgressSpacePrivileges(inProgressSpacePrivileges: any[]) { diff --git a/x-pack/plugins/security/public/views/management/management.js b/x-pack/plugins/security/public/views/management/management.js index a193280c1e8c541..7911d3a48bd1c1d 100644 --- a/x-pack/plugins/security/public/views/management/management.js +++ b/x-pack/plugins/security/public/views/management/management.js @@ -16,6 +16,7 @@ import '../../services/shield_user'; import { ROLES_PATH, USERS_PATH } from './management_urls'; import { management } from 'ui/management'; +import { i18n } from '@kbn/i18n'; routes.defaults(/\/management/, { resolve: { @@ -29,7 +30,10 @@ routes.defaults(/\/management/, { function ensureSecurityRegistered() { const registerSecurity = () => management.register('security', { - display: 'Security', + display: i18n.translate( + 'xpack.security.management.securityTitle', { + defaultMessage: 'Security', + }), order: 10, icon: 'securityApp', }); @@ -41,7 +45,10 @@ routes.defaults(/\/management/, { security.register('users', { name: 'securityUsersLink', order: 10, - display: 'Users', + display: i18n.translate( + 'xpack.security.management.usersTitle', { + defaultMessage: 'Users', + }), url: `#${USERS_PATH}`, }); } @@ -50,7 +57,10 @@ routes.defaults(/\/management/, { security.register('roles', { name: 'securityRolesLink', order: 20, - display: 'Roles', + display: i18n.translate( + 'xpack.security.management.rolesTitle', { + defaultMessage: 'Roles', + }), url: `#${ROLES_PATH}`, }); } diff --git a/x-pack/plugins/security/public/views/management/roles.html b/x-pack/plugins/security/public/views/management/roles.html index 941ab6ed331b254..a7b77228c037609 100644 --- a/x-pack/plugins/security/public/views/management/roles.html +++ b/x-pack/plugins/security/public/views/management/roles.html @@ -3,15 +3,19 @@
- - You do not have permission to manage roles. - +
-
- Please contact your administrator. -
+
@@ -30,7 +34,7 @@ aria-label="Filter" ng-model="query" > -
+
@@ -42,7 +46,10 @@ > - Delete + @@ -54,7 +61,11 @@ ng-if="!selectedRoles.length" data-test-subj="createRoleButton" > - Create role + +
@@ -65,8 +76,23 @@
-
- No matching roles found. +
+ + +
@@ -97,7 +123,10 @@ aria-label="{{sort.reverse ? 'Sort role ascending' : 'Sort role descending'}}" > - Role +
@@ -175,9 +212,13 @@
-
- {{ selectedRoles.length }} roles selected -
+
diff --git a/x-pack/plugins/security/public/views/management/roles.js b/x-pack/plugins/security/public/views/management/roles.js index c60b9905ea68f68..98e08af55ea96ee 100644 --- a/x-pack/plugins/security/public/views/management/roles.js +++ b/x-pack/plugins/security/public/views/management/roles.js @@ -19,25 +19,34 @@ routes.when(ROLES_PATH, { resolve: { roles(ShieldRole, kbnUrl, Promise, Private) { // $promise is used here because the result is an ngResource, not a promise itself - return ShieldRole.query().$promise - .catch(checkLicenseError(kbnUrl, Promise, Private)) + return ShieldRole.query() + .$promise.catch(checkLicenseError(kbnUrl, Promise, Private)) .catch(_.identity); // Return the error if there is one - } + }, }, - controller($scope, $route, $q, confirmModal) { + controller($scope, $route, $q, confirmModal, i18n) { $scope.roles = $route.current.locals.roles; $scope.forbidden = !_.isArray($scope.roles); $scope.selectedRoles = []; $scope.sort = { orderBy: 'name', reverse: false }; $scope.editRolesHref = `#${EDIT_ROLES_PATH}`; - $scope.getEditRoleHref = (role) => `#${EDIT_ROLES_PATH}/${role}`; + $scope.getEditRoleHref = role => `#${EDIT_ROLES_PATH}/${role}`; $scope.deleteRoles = () => { const doDelete = () => { - $q.all($scope.selectedRoles.map((role) => role.$delete())) - .then(() => toastNotifications.addSuccess(`Deleted ${$scope.selectedRoles.length > 1 ? 'roles' : 'role'}`)) + $q.all($scope.selectedRoles.map(role => role.$delete())) + .then(() => + toastNotifications.addSuccess( + i18n('xpack.security.management.roles.deleteRoleTitle', { + defaultMessage: 'Deleted {value, plural, one {role} other {roles}}', + values: { + value: $scope.selectedRoles.length, + }, + }) + ) + ) .then(() => { - $scope.selectedRoles.map((role) => { + $scope.selectedRoles.map(role => { const i = $scope.roles.indexOf(role); $scope.roles.splice(i, 1); }); @@ -45,12 +54,17 @@ routes.when(ROLES_PATH, { }); }; const confirmModalOptions = { - confirmButtonText: 'Delete role(s)', - onConfirm: doDelete + confirmButtonText: i18n('xpack.security.management.roles.deleteRoleConfirmButtonLabel', { + defaultMessage: 'Delete role(s)', + }), + onConfirm: doDelete, }; - confirmModal(` - Are you sure you want to delete the selected role(s)? This action is irreversible!`, - confirmModalOptions + confirmModal( + i18n('xpack.security.management.roles.deletingRolesWarningMessage', { + defaultMessage: + 'Are you sure you want to delete the selected role(s)? This action is irreversible!', + }), + confirmModalOptions ); }; @@ -83,7 +97,16 @@ routes.when(ROLES_PATH, { $scope.toggleSort = toggleSort; function getActionableRoles() { - return $scope.roles.filter((role) => !role.metadata._reserved); + return $scope.roles.filter(role => !role.metadata._reserved); } - } + + $scope.reversedTooltip = i18n('xpack.security.management.roles.reversedTooltip', { + defaultMessage: 'Reserved roles are built-in and cannot be removed or modified. Only the password may be changed.', + }); + + $scope.reversedAriaLabel = i18n('xpack.security.management.roles.reversedAriaLabel', { + defaultMessage: 'Reserved roles are built-in and cannot be removed or modified. Only the password may be changed.', + }); + + }, }); diff --git a/x-pack/plugins/security/server/lib/__tests__/__fixtures__/request.js b/x-pack/plugins/security/server/lib/__tests__/__fixtures__/request.js index d3ba20e4d5b6137..fbd088dcc958874 100644 --- a/x-pack/plugins/security/server/lib/__tests__/__fixtures__/request.js +++ b/x-pack/plugins/security/server/lib/__tests__/__fixtures__/request.js @@ -8,6 +8,7 @@ import url from 'url'; export function requestFixture({ headers = { accept: 'something/html' }, path = '/wat', + basePath = '', search = '', payload } = {}) { @@ -15,6 +16,7 @@ export function requestFixture({ raw: { req: { headers } }, headers, url: { path, search }, + getBasePath: () => basePath, query: search ? url.parse(search, { parseQueryString: true }).query : {}, payload, state: { user: 'these are the contents of the user client cookie' } diff --git a/x-pack/plugins/security/server/lib/authentication/providers/__tests__/basic.js b/x-pack/plugins/security/server/lib/authentication/providers/__tests__/basic.js index 02c2df354232ad2..931728a13981423 100644 --- a/x-pack/plugins/security/server/lib/authentication/providers/__tests__/basic.js +++ b/x-pack/plugins/security/server/lib/authentication/providers/__tests__/basic.js @@ -44,13 +44,13 @@ describe('BasicAuthenticationProvider', () => { it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { const authenticationResult = await provider.authenticate( - requestFixture({ path: '/some-path # that needs to be encoded' }), + requestFixture({ path: '/some-path # that needs to be encoded', basePath: '/s/foo' }), null ); expect(authenticationResult.redirected()).to.be(true); expect(authenticationResult.redirectURL).to.be( - '/base-path/login?next=%2Fbase-path%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + '/base-path/login?next=%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' ); }); diff --git a/x-pack/plugins/security/server/lib/authentication/providers/__tests__/saml.js b/x-pack/plugins/security/server/lib/authentication/providers/__tests__/saml.js index 2716c3d7598a9f5..0000afe96a0b508 100644 --- a/x-pack/plugins/security/server/lib/authentication/providers/__tests__/saml.js +++ b/x-pack/plugins/security/server/lib/authentication/providers/__tests__/saml.js @@ -40,7 +40,7 @@ describe('SAMLAuthenticationProvider', () => { }); it('redirects non-AJAX request that can not be authenticated to the IdP.', async () => { - const request = requestFixture({ path: '/some-path' }); + const request = requestFixture({ path: '/some-path', basePath: '/s/foo' }); callWithInternalUser .withArgs('shield.samlPrepare') @@ -61,7 +61,7 @@ describe('SAMLAuthenticationProvider', () => { expect(authenticationResult.redirectURL).to.be('https://idp-host/path/login?SAMLRequest=some%20request%20'); expect(authenticationResult.state).to.eql({ requestId: 'some-request-id', - nextURL: `/test-base-path/some-path` + nextURL: `/s/foo/some-path` }); }); @@ -334,7 +334,7 @@ describe('SAMLAuthenticationProvider', () => { }); it('initiates SAML handshake for non-AJAX requests if refresh token is used more than once.', async () => { - const request = requestFixture({ path: '/some-path' }); + const request = requestFixture({ path: '/some-path', basePath: '/s/foo' }); callWithInternalUser .withArgs('shield.samlPrepare') @@ -372,7 +372,7 @@ describe('SAMLAuthenticationProvider', () => { expect(authenticationResult.redirectURL).to.be('https://idp-host/path/login?SAMLRequest=some%20request%20'); expect(authenticationResult.state).to.eql({ requestId: 'some-request-id', - nextURL: `/test-base-path/some-path` + nextURL: `/s/foo/some-path` }); }); @@ -404,7 +404,7 @@ describe('SAMLAuthenticationProvider', () => { }); it('initiates SAML handshake for non-AJAX requests if refresh token is expired.', async () => { - const request = requestFixture({ path: '/some-path' }); + const request = requestFixture({ path: '/some-path', basePath: '/s/foo' }); callWithInternalUser .withArgs('shield.samlPrepare') @@ -442,7 +442,7 @@ describe('SAMLAuthenticationProvider', () => { expect(authenticationResult.redirectURL).to.be('https://idp-host/path/login?SAMLRequest=some%20request%20'); expect(authenticationResult.state).to.eql({ requestId: 'some-request-id', - nextURL: `/test-base-path/some-path` + nextURL: `/s/foo/some-path` }); }); diff --git a/x-pack/plugins/security/server/lib/authentication/providers/basic.js b/x-pack/plugins/security/server/lib/authentication/providers/basic.js index 756b8a6ee10cee7..87b193c9e053e09 100644 --- a/x-pack/plugins/security/server/lib/authentication/providers/basic.js +++ b/x-pack/plugins/security/server/lib/authentication/providers/basic.js @@ -55,7 +55,7 @@ export class BasicAuthenticationProvider { authenticationResult = await this._authenticateViaState(request, state); } else if (authenticationResult.notHandled() && canRedirectRequest(request)) { // If we couldn't handle authentication let's redirect user to the login page. - const nextURL = encodeURIComponent(`${this._options.basePath}${request.url.path}`); + const nextURL = encodeURIComponent(`${request.getBasePath()}${request.url.path}`); authenticationResult = AuthenticationResult.redirectTo( `${this._options.basePath}/login?next=${nextURL}` ); diff --git a/x-pack/plugins/security/server/lib/authentication/providers/saml.js b/x-pack/plugins/security/server/lib/authentication/providers/saml.js index 0319c84f1825f48..fc364736e395b9d 100644 --- a/x-pack/plugins/security/server/lib/authentication/providers/saml.js +++ b/x-pack/plugins/security/server/lib/authentication/providers/saml.js @@ -357,7 +357,7 @@ export class SAMLAuthenticationProvider { return AuthenticationResult.redirectTo( redirect, // Store request id in the state so that we can reuse it once we receive `SAMLResponse`. - { requestId, nextURL: `${this._options.basePath}${request.url.path}` } + { requestId, nextURL: `${request.getBasePath()}${request.url.path}` } ); } catch (err) { this._options.log(['debug', 'security', 'saml'], `Failed to initiate SAML handshake: ${err.message}`); diff --git a/x-pack/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap b/x-pack/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap index 564c8aac780207e..3edeb75929e4f70 100644 --- a/x-pack/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap +++ b/x-pack/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap @@ -11,6 +11,10 @@ exports[`ManageSpacesButton renders as expected 1`] = ` size="s" type="button" > - Manage spaces + `; diff --git a/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx b/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx index 374ffc5d3650030..2c0c42f953ab89f 100644 --- a/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx +++ b/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx @@ -3,8 +3,8 @@ * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { UserProfileProvider } from '../../../xpack_main/public/services/user_profile'; import { ManageSpacesButton } from './manage_spaces_button'; @@ -15,11 +15,11 @@ const buildUserProfile = (canManageSpaces: boolean) => { describe('ManageSpacesButton', () => { it('renders as expected', () => { const component = ; - expect(shallow(component)).toMatchSnapshot(); + expect(shallowWithIntl(component)).toMatchSnapshot(); }); it(`doesn't render if user profile forbids managing spaces`, () => { const component = ; - expect(shallow(component)).toMatchSnapshot(); + expect(shallowWithIntl(component)).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx b/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx index 008af659e871c40..a9e40a89edacf00 100644 --- a/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx @@ -5,6 +5,7 @@ */ import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, CSSProperties } from 'react'; import { UserProfile } from '../../../xpack_main/public/services/user_profile'; import { MANAGE_SPACES_URL } from '../lib/constants'; @@ -32,7 +33,10 @@ export class ManageSpacesButton extends Component { onClick={this.navigateToManageSpaces} style={this.props.style} > - Manage spaces + ); } diff --git a/x-pack/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/plugins/spaces/public/lib/spaces_manager.ts index 8a11e0137897f88..1eb30a8e5f86462 100644 --- a/x-pack/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/lib/spaces_manager.ts @@ -3,6 +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 { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; import { IHttpResponse } from 'angular'; @@ -66,8 +67,12 @@ export class SpacesManager extends EventEmitter { public _displayError() { toastNotifications.addDanger({ - title: 'Unable to change your Space', - text: 'please try again later', + title: i18n.translate('xpack.spaces.spacesManager.unableToChangeSpaceWarningTitle', { + defaultMessage: 'Unable to change your Space', + }), + text: i18n.translate('xpack.spaces.spacesManager.unableToChangeSpaceWarningDescription', { + defaultMessage: 'please try again later', + }), }); } } diff --git a/x-pack/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap index 25b15e58a2fb0e4..e916272f8110eca 100644 --- a/x-pack/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -11,8 +11,15 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` - Delete space - 'My Space' + @@ -22,12 +29,21 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` size="m" >

- Deleting a space permanently removes the space and - - - all of its contents - - . You can't undo this action. + + + , + } + } + />

- You are about to delete your current space - - ( - - My Space - - ) - - . You will be redirected to choose a different space if you continue. + + ( + + My Space + + ) + , + } + } + /> @@ -76,7 +98,11 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` onClick={[MockFunction]} type="button" > - Cancel + - Delete space and all contents + diff --git a/x-pack/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap index 673576cd01b3f6d..b53b4d6ec7caf0a 100644 --- a/x-pack/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap @@ -6,14 +6,22 @@ exports[`UnauthorizedPrompt renders as expected 1`] = `

- You do not have permission to manage spaces. +

} iconColor="danger" iconType="spacesApp" title={

- Permission denied +

} /> diff --git a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap index 0d539fffe6e348e..1f91e57a3f37e8e 100644 --- a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap @@ -11,11 +11,17 @@ exports[`AdvancedSettingsSubtitle renders as expected 1`] = ` size="m" title={

- The settings on this page apply to the - - My Space - - space, unless otherwise specified. + + My Space + , + } + } + />

} /> diff --git a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx index 39444cf01e88b74..dadc900021dda8b 100644 --- a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx @@ -3,8 +3,8 @@ * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { AdvancedSettingsSubtitle } from './advanced_settings_subtitle'; describe('AdvancedSettingsSubtitle', () => { @@ -13,6 +13,6 @@ describe('AdvancedSettingsSubtitle', () => { id: 'my-space', name: 'My Space', }; - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx index 0eef1703fc85634..901bce019012f30 100644 --- a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx @@ -5,6 +5,7 @@ */ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { Space } from '../../../../../common/model/space'; @@ -20,8 +21,13 @@ export const AdvancedSettingsSubtitle = (props: Props) => ( iconType="spacesApp" title={

- The settings on this page apply to the {props.space.name} space, unless - otherwise specified. + {props.space.name}, + }} + />

} /> diff --git a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap index 6a8bcdf8a9f5317..eb5f3eac933581d 100644 --- a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap @@ -39,7 +39,11 @@ exports[`AdvancedSettingsTitle renders as expected 1`] = `

- Settings +

diff --git a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx index d46848f2c103fa7..9e779ad5a44e360 100644 --- a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx @@ -3,8 +3,8 @@ * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { AdvancedSettingsTitle } from './advanced_settings_title'; describe('AdvancedSettingsTitle', () => { @@ -13,6 +13,6 @@ describe('AdvancedSettingsTitle', () => { id: 'my-space', name: 'My Space', }; - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx index c0a4924e9c072ae..eaa952f7d3f7d79 100644 --- a/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx @@ -5,6 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { Space } from '../../../../../common/model/space'; import { SpaceAvatar } from '../../../../components'; @@ -20,7 +21,12 @@ export const AdvancedSettingsTitle = (props: Props) => ( -

Settings

+

+ +

diff --git a/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx b/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx index 2fbfd6da452e4cd..49ffbcecbbe8805 100644 --- a/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { SpacesManager } from '../../../lib'; import { SpacesNavState } from '../../nav_control'; import { ConfirmDeleteModal } from './confirm_delete_modal'; @@ -38,13 +38,14 @@ describe('ConfirmDeleteModal', () => { const onConfirm = jest.fn(); expect( - shallow( - ) ).toMatchSnapshot(); @@ -71,13 +72,14 @@ describe('ConfirmDeleteModal', () => { const onCancel = jest.fn(); const onConfirm = jest.fn(); - const wrapper = mount( - ); diff --git a/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx b/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx index 2c884e03ece2b92..c8e804efeda9296 100644 --- a/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx @@ -20,6 +20,7 @@ import { EuiOverlayMask, EuiText, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { SpacesNavState } from 'plugins/spaces/views/nav_control'; import React, { ChangeEvent, Component } from 'react'; import { Space } from '../../../../common/model/space'; @@ -31,6 +32,7 @@ interface Props { spacesNavState: SpacesNavState; onCancel: () => void; onConfirm: () => void; + intl: InjectedIntl; } interface State { @@ -39,7 +41,7 @@ interface State { deleteInProgress: boolean; } -export class ConfirmDeleteModal extends Component { +class ConfirmDeleteModalUI extends Component { public state = { confirmSpaceName: '', error: null, @@ -47,7 +49,7 @@ export class ConfirmDeleteModal extends Component { }; public render() { - const { space, spacesNavState, onCancel } = this.props; + const { space, spacesNavState, onCancel, intl } = this.props; let warning = null; if (isDeletingCurrentSpace(space, spacesNavState)) { @@ -59,8 +61,11 @@ export class ConfirmDeleteModal extends Component { warning = ( - You are about to delete your current space {name}. You will be redirected to choose a - different space if you continue. + ); @@ -74,20 +79,44 @@ export class ConfirmDeleteModal extends Component { - Delete space {`'${space.name}'`} +

- Deleting a space permanently removes the space and{' '} - all of its contents. You can't undo this action. + + + + ), + }} + />

{ onClick={onCancel} isDisabled={this.state.deleteInProgress} > - Cancel + { color={'danger'} isLoading={this.state.deleteInProgress} > - Delete space and all contents +
@@ -165,3 +200,5 @@ export class ConfirmDeleteModal extends Component { function isDeletingCurrentSpace(space: Space, spacesNavState: SpacesNavState) { return space.id === spacesNavState.getActiveSpace().id; } + +export const ConfirmDeleteModal = injectI18n(ConfirmDeleteModalUI); diff --git a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap index e8616433d5b72c7..a18d45e2d6518dd 100644 --- a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap @@ -13,16 +13,25 @@ exports[`SecureSpaceMessage renders if user profile allows security to be manage size="m" >

- Want to assign a role to a space? Go to Management and select - - - Roles - - . + + + , + } + } + />

diff --git a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx index 65b6bc2c120061a..95aa3b37cf24cb7 100644 --- a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx @@ -3,8 +3,8 @@ * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { SecureSpaceMessage } from './secure_space_message'; describe('SecureSpaceMessage', () => { @@ -18,7 +18,7 @@ describe('SecureSpaceMessage', () => { }, }; - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); it(`renders if user profile allows security to be managed`, () => { @@ -31,6 +31,6 @@ describe('SecureSpaceMessage', () => { }, }; - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx index db77c1ad6d113ae..30fb1f1458355d9 100644 --- a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx @@ -5,6 +5,7 @@ */ import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { UserProfile } from 'plugins/xpack_main/services/user_profile'; import React, { Fragment } from 'react'; @@ -19,8 +20,20 @@ export const SecureSpaceMessage = (props: Props) => {

- Want to assign a role to a space? Go to Management and select{' '} - Roles. + + + + ), + }} + />

diff --git a/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx b/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx index 35fdd69f64e8b43..a2b4c008ac175d4 100644 --- a/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx @@ -3,12 +3,12 @@ * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { UnauthorizedPrompt } from './unauthorized_prompt'; describe('UnauthorizedPrompt', () => { it('renders as expected', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx b/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx index a0888b86fa0eb72..dd3f96d94eab434 100644 --- a/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx @@ -5,15 +5,28 @@ */ import { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; export const UnauthorizedPrompt = () => ( Permission denied} + title={ +

+ +

+ } body={ -

You do not have permission to manage spaces.

+

+ +

} /> ); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/customize_space_avatar.test.tsx.snap index cbc3efafff90e46..14f1b31f4d194b7 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/customize_space_avatar.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/customize_space_avatar.test.tsx.snap @@ -16,7 +16,11 @@ exports[`renders without crashing 1`] = ` onClick={[Function]} type="button" > - Customize +
diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap index efb34025573aa10..93bb0363ed65ad3 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap @@ -10,7 +10,11 @@ exports[`DeleteSpacesButton renders as expected 1`] = ` onClick={[Function]} type="button" > - Delete space + `; diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.tsx.snap index cf7e0941e4ad357..b22754203b92abd 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.tsx.snap @@ -8,29 +8,50 @@ exports[`renders without crashing 1`] = ` hasEmptyLabelSpace={false} helpText={

- If the identifier is - - engineering - - , the Kibana URL is -
- https://my-kibana.example - - /s/engineering/ - - app/kibana. + + + , + "engineeringKibanaUrl": + https://my-kibana.example + + /s/engineering/ + + app/kibana + , + "nextLine":
, + } + } + />

} isInvalid={false} label={

- URL identifier + - [edit] +

} diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.test.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.test.tsx index 180ed9b0dfef756..a41cd8829f157d3 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.test.tsx @@ -5,8 +5,8 @@ */ // @ts-ignore import { EuiColorPicker, EuiFieldText, EuiLink } from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { CustomizeSpaceAvatar } from './customize_space_avatar'; const space = { @@ -15,17 +15,23 @@ const space = { }; test('renders without crashing', () => { - const wrapper = shallow(); + const wrapper = shallowWithIntl( + + ); expect(wrapper).toMatchSnapshot(); }); test('renders a "customize" link by default', () => { - const wrapper = mount(); + const wrapper = mountWithIntl( + + ); expect(wrapper.find(EuiLink)).toHaveLength(1); }); test('shows customization fields when the "customize" link is clicked', () => { - const wrapper = mount(); + const wrapper = mountWithIntl( + + ); wrapper.find(EuiLink).simulate('click'); expect(wrapper.find(EuiLink)).toHaveLength(0); @@ -43,7 +49,13 @@ test('invokes onChange callback when avatar is customized', () => { const changeHandler = jest.fn(); - const wrapper = mount(); + const wrapper = mountWithIntl( + + ); wrapper.find(EuiLink).simulate('click'); wrapper diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.tsx index 09d28cd431e8113..cdbd9aa8d8af58f 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/customize_space_avatar.tsx @@ -5,6 +5,7 @@ */ // @ts-ignore import { EuiColorPicker, EuiFieldText, EuiFlexItem, EuiFormRow, EuiLink } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; import { MAX_SPACE_INITIALS } from '../../../../common/constants'; import { Space } from '../../../../common/model/space'; @@ -13,6 +14,7 @@ import { getSpaceColor, getSpaceInitials } from '../../../../common/space_attrib interface Props { space: Partial; onChange: (space: Partial) => void; + intl: InjectedIntl; } interface State { @@ -21,7 +23,7 @@ interface State { pendingInitials?: string | null; } -export class CustomizeSpaceAvatar extends Component { +class CustomizeSpaceAvatarUI extends Component { private initialsRef: HTMLInputElement | null = null; constructor(props: Props) { @@ -37,14 +39,19 @@ export class CustomizeSpaceAvatar extends Component { } public getCustomizeFields = () => { - const { space } = this.props; + const { space, intl } = this.props; const { initialsHasFocus, pendingInitials } = this.state; return ( - + { - + @@ -97,7 +109,10 @@ export class CustomizeSpaceAvatar extends Component { - Customize + @@ -130,3 +145,5 @@ export class CustomizeSpaceAvatar extends Component { }); }; } + +export const CustomizeSpaceAvatar = injectI18n(CustomizeSpaceAvatarUI); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx index 456a6d1ccbb4366..5867abf78997fae 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { SpacesManager } from '../../../lib'; import { SpacesNavState } from '../../nav_control'; import { DeleteSpacesButton } from './delete_spaces_button'; @@ -34,12 +34,13 @@ describe('DeleteSpacesButton', () => { refreshSpacesList: jest.fn(), }; - const wrapper = shallow( - ); expect(wrapper).toMatchSnapshot(); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx index 25aea985e01ac39..7f3dd0aea485ec8 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx @@ -5,6 +5,7 @@ */ import { EuiButton, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { SpacesNavState } from 'plugins/spaces/views/nav_control'; import React, { Component, Fragment } from 'react'; // @ts-ignore @@ -19,6 +20,7 @@ interface Props { spacesManager: SpacesManager; spacesNavState: SpacesNavState; onDelete: () => void; + intl: InjectedIntl; } interface State { @@ -26,14 +28,20 @@ interface State { showConfirmRedirectModal: boolean; } -export class DeleteSpacesButton extends Component { +class DeleteSpacesButtonUI extends Component { public state = { showConfirmDeleteModal: false, showConfirmRedirectModal: false, }; public render() { - const buttonText = `Delete space`; + const buttonText = ( + + ); + const { intl } = this.props; let ButtonComponent: any = EuiButton; @@ -49,7 +57,10 @@ export class DeleteSpacesButton extends Component { {buttonText} @@ -88,21 +99,40 @@ export class DeleteSpacesButton extends Component { }; public deleteSpaces = async () => { - const { spacesManager, space, spacesNavState } = this.props; + const { spacesManager, space, spacesNavState, intl } = this.props; try { await spacesManager.deleteSpace(space); } catch (error) { const { message: errorMessage = '' } = error.data || {}; - toastNotifications.addDanger(`Error deleting space: ${errorMessage}`); + toastNotifications.addDanger( + intl.formatMessage( + { + id: 'xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle', + defaultMessage: 'Error deleting space: {errorMessage}', + }, + { + errorMessage, + } + ) + ); } this.setState({ showConfirmDeleteModal: false, }); - const message = `Deleted "${space.name}" space.`; + const message = intl.formatMessage( + { + id: + 'xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage', + defaultMessage: 'Deleted {spaceName} space.', + }, + { + spaceName: space.name, + } + ); toastNotifications.addSuccess(message); @@ -113,3 +143,5 @@ export class DeleteSpacesButton extends Component { spacesNavState.refreshSpacesList(); }; } + +export const DeleteSpacesButton = injectI18n(DeleteSpacesButtonUI); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx index e560af2d3d99a67..e89f08d167e6ff8 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { UserProfileProvider } from '../../../../../xpack_main/public/services/user_profile'; import { SpacesManager } from '../../../lib'; import { SpacesNavState } from '../../nav_control'; @@ -42,11 +42,12 @@ describe('ManageSpacePage', () => { const userProfile = buildUserProfile(true); - const wrapper = mount( - ); const nameInput = wrapper.find('input[name="name"]'); @@ -96,12 +97,13 @@ describe('ManageSpacePage', () => { const userProfile = buildUserProfile(true); - const wrapper = mount( - ); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx index 8371756d0b8a31e..8a2398e695dd255 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx @@ -21,6 +21,7 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; import { SpacesNavState } from 'plugins/spaces/views/nav_control'; @@ -45,6 +46,7 @@ interface Props { spaceId?: string; userProfile: UserProfile; spacesNavState: SpacesNavState; + intl: InjectedIntl; } interface State { @@ -56,7 +58,7 @@ interface State { }; } -export class ManageSpacePage extends Component { +class ManageSpacePageUI extends Component { private readonly validator: SpaceValidator; constructor(props: Props) { @@ -69,7 +71,7 @@ export class ManageSpacePage extends Component { } public componentDidMount() { - const { spaceId, spacesManager } = this.props; + const { spaceId, spacesManager, intl } = this.props; if (spaceId) { spacesManager @@ -85,7 +87,17 @@ export class ManageSpacePage extends Component { .catch(error => { const { message = '' } = error.data || {}; - toastNotifications.addDanger(`Error loading space: ${message}`); + toastNotifications.addDanger( + intl.formatMessage( + { + id: 'xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle', + defaultMessage: 'Error loading space: {message}', + }, + { + message, + } + ) + ); this.backToSpacesList(); }); } else { @@ -113,14 +125,19 @@ export class ManageSpacePage extends Component {
{' '} -

Loading...

+

+ +

); }; public getForm = () => { - const { userProfile } = this.props; + const { userProfile, intl } = this.props; if (!userProfile.hasCapability('manageSpaces')) { return ; @@ -134,16 +151,25 @@ export class ManageSpacePage extends Component { - + - {name && ( @@ -170,13 +196,19 @@ export class ManageSpacePage extends Component { )} { public getTitle = () => { if (this.editingExistingSpace()) { - return `Edit space`; + return ( + + ); } - return `Create space`; + return ( + + ); }; public maybeGetSecureSpacesMessage = () => { @@ -215,7 +257,17 @@ export class ManageSpacePage extends Component { }; public getFormButtons = () => { - const saveText = this.editingExistingSpace() ? 'Update space' : 'Create space'; + const saveText = this.editingExistingSpace() ? ( + + ) : ( + + ); return ( @@ -225,7 +277,10 @@ export class ManageSpacePage extends Component { - Cancel + @@ -314,6 +369,7 @@ export class ManageSpacePage extends Component { }; private performSave = () => { + const { intl } = this.props; if (!this.state.space) { return; } @@ -339,13 +395,34 @@ export class ManageSpacePage extends Component { action .then(() => { this.props.spacesNavState.refreshSpacesList(); - toastNotifications.addSuccess(`'${name}' was saved`); + toastNotifications.addSuccess( + intl.formatMessage( + { + id: + 'xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage', + defaultMessage: '{name} was saved', + }, + { + name: `'${name}'`, + } + ) + ); window.location.hash = `#/management/spaces/list`; }) .catch(error => { const { message = '' } = error.data || {}; - toastNotifications.addDanger(`Error saving space: ${message}`); + toastNotifications.addDanger( + intl.formatMessage( + { + id: 'xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle', + defaultMessage: 'Error saving space: {message}', + }, + { + message, + } + ) + ); }); }; @@ -355,3 +432,5 @@ export class ManageSpacePage extends Component { private editingExistingSpace = () => !!this.props.spaceId; } + +export const ManageSpacePage = injectI18n(ManageSpacePageUI); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx index ae584ba715b63b9..7a180f39237a6e3 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx @@ -5,8 +5,8 @@ */ import { EuiIcon } from '@elastic/eui'; -import { shallow } from 'enzyme'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ReservedSpaceBadge } from './reserved_space_badge'; const reservedSpace = { @@ -21,11 +21,11 @@ const unreservedSpace = { }; test('it renders without crashing', () => { - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper.find(EuiIcon)).toHaveLength(1); }); test('it renders nothing for an unreserved space', () => { - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper.find('*')).toHaveLength(0); }); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx index 5c358cb4fd5caa3..e4b2dda3a668b5e 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { isReservedSpace } from '../../../../common'; import { Space } from '../../../../common/model/space'; @@ -19,7 +20,14 @@ export const ReservedSpaceBadge = (props: Props) => { if (space && isReservedSpace(space)) { return ( - + + } + > ); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.test.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.test.tsx index f4f9b11f94b2d25..75064391aad0528 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { SpaceValidator } from '../lib'; import { SpaceIdentifier } from './space_identifier'; @@ -19,6 +19,8 @@ test('renders without crashing', () => { onChange: jest.fn(), validator: new SpaceValidator(), }; - const wrapper = shallow(); + const wrapper = shallowWithIntl( + + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.tsx index cfd7639bac2bed2..b23c1fd9d68e38d 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.tsx @@ -9,6 +9,7 @@ import { EuiFormRow, EuiLink, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; import { Space } from '../../../../common/model/space'; import { SpaceValidator } from '../lib'; @@ -18,13 +19,14 @@ interface Props { editable: boolean; validator: SpaceValidator; onChange: (e: ChangeEvent) => void; + intl: InjectedIntl; } interface State { editing: boolean; } -export class SpaceIdentifier extends Component { +class SpaceIdentifierUI extends Component { private textFieldRef: HTMLInputElement | null = null; @@ -36,6 +38,7 @@ export class SpaceIdentifier extends Component { } public render() { + const { intl } = this.props; const { id = '' } = this.props.space; @@ -53,7 +56,11 @@ export class SpaceIdentifier extends Component { placeholder={ this.state.editing || !this.props.editable ? undefined - : 'The URL identifier is generated from the space name.' + : intl.formatMessage({ + id: + 'xpack.spaces.management.spaceIdentifier.urlIdentifierGeneratedFromSpaceNameTooltip', + defaultMessage: 'The URL identifier is generated from the space name.', + }) } value={id} onChange={this.onChange} @@ -67,15 +74,63 @@ export class SpaceIdentifier extends Component { public getLabel = () => { if (!this.props.editable) { - return (

URL identifier

); + return ( +

+ +

); } - const editLinkText = this.state.editing ? `[stop editing]` : `[edit]`; - return (

URL identifier {editLinkText}

); + const editLinkText = this.state.editing ? ( + + ) : ( + + ); + return ( +

+ + {editLinkText} +

+ ); }; public getHelpText = () => { - return (

If the identifier is engineering, the Kibana URL is
https://my-kibana.example/s/engineering/app/kibana.

); + return ( +

+ + + + ), + nextLine:
, + engineeringKibanaUrl: ( + + https://my-kibana.example/s/engineering/app/kibana + + ), + }} + /> +

+ ); }; public onEditClick = () => { @@ -93,3 +148,5 @@ export class SpaceIdentifier extends Component { this.props.onChange(e); }; } + +export const SpaceIdentifier = injectI18n(SpaceIdentifierUI); diff --git a/x-pack/plugins/spaces/public/views/management/lib/validate_space.ts b/x-pack/plugins/spaces/public/views/management/lib/validate_space.ts index fa47822334da74f..cc935ce653b58db 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/validate_space.ts +++ b/x-pack/plugins/spaces/public/views/management/lib/validate_space.ts @@ -3,6 +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 { i18n } from '@kbn/i18n'; import { isReservedSpace } from '../../../../common/is_reserved_space'; import { Space } from '../../../../common/model/space'; import { isValidSpaceIdentifier } from './space_identifier_utils'; @@ -32,11 +33,19 @@ export class SpaceValidator { } if (!space.name || !space.name.trim()) { - return invalid(`Name is required`); + return invalid( + i18n.translate('xpack.spaces.management.validateSpace.requiredNameErrorMessage', { + defaultMessage: 'Name is required', + }) + ); } if (space.name.length > 1024) { - return invalid(`Name must not exceed 1024 characters`); + return invalid( + i18n.translate('xpack.spaces.management.validateSpace.nameMaxLengthErrorMessage', { + defaultMessage: 'Name must not exceed 1024 characters', + }) + ); } return valid(); @@ -48,7 +57,11 @@ export class SpaceValidator { } if (space.description && space.description.length > 2000) { - return invalid(`Description must not exceed 2000 characters`); + return invalid( + i18n.translate('xpack.spaces.management.validateSpace.describeMaxLengthErrorMessage', { + defaultMessage: 'Description must not exceed 2000 characters', + }) + ); } return valid(); @@ -64,11 +77,23 @@ export class SpaceValidator { } if (!space.id) { - return invalid(`URL identifier is required`); + return invalid( + i18n.translate('xpack.spaces.management.validateSpace.urlIdentifierRequiredErrorMessage', { + defaultMessage: 'URL identifier is required', + }) + ); } if (!isValidSpaceIdentifier(space.id)) { - return invalid('URL identifier can only contain a-z, 0-9, and the characters "_" and "-"'); + return invalid( + i18n.translate( + 'xpack.spaces.management.validateSpace.urlIdentifierAllowedCharactersErrorMessage', + { + defaultMessage: + 'URL identifier can only contain a-z, 0-9, and the characters "_" and "-"', + } + ) + ); } return valid(); diff --git a/x-pack/plugins/spaces/public/views/management/page_routes.tsx b/x-pack/plugins/spaces/public/views/management/page_routes.tsx index d4f6bce394baa38..1577aaf0481c942 100644 --- a/x-pack/plugins/spaces/public/views/management/page_routes.tsx +++ b/x-pack/plugins/spaces/public/views/management/page_routes.tsx @@ -3,6 +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 { I18nProvider } from '@kbn/i18n/react'; // @ts-ignore import template from 'plugins/spaces/views/management/template.html'; @@ -39,11 +40,13 @@ routes.when('/management/spaces/list', { const spacesManager = new SpacesManager($http, chrome, spaceSelectorURL); render( - , + + + , domNode ); @@ -75,11 +78,13 @@ routes.when('/management/spaces/create', { const spacesManager = new SpacesManager($http, chrome, spaceSelectorURL); render( - , + + + , domNode ); @@ -118,12 +123,14 @@ routes.when('/management/spaces/edit/:spaceId', { const spacesManager = new SpacesManager($http, chrome, spaceSelectorURL); render( - , + + + , domNode ); diff --git a/x-pack/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap b/x-pack/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap index 1f719a5a6126814..bebda86fa9721af 100644 --- a/x-pack/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap @@ -31,7 +31,11 @@ exports[`SpacesGridPage renders as expected 1`] = ` size="m" >

- Spaces +

@@ -46,7 +50,11 @@ exports[`SpacesGridPage renders as expected 1`] = ` onClick={[Function]} type="button" > - Create space +
@@ -107,7 +115,13 @@ exports[`SpacesGridPage renders as expected 1`] = ` itemId="id" items={Array []} loading={true} - message="loading..." + message={ + + } pagination={true} responsive={true} search={ diff --git a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx index d61c4113366cbf5..6a840680104aa5d 100644 --- a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx @@ -19,6 +19,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; // @ts-ignore import { toastNotifications } from 'ui/notify'; @@ -36,6 +37,7 @@ interface Props { spacesManager: SpacesManager; spacesNavState: SpacesNavState; userProfile: UserProfile; + intl: InjectedIntl; } interface State { @@ -46,7 +48,7 @@ interface State { error: Error | null; } -export class SpacesGridPage extends Component { +class SpacesGridPageUI extends Component { constructor(props: Props) { super(props); this.state = { @@ -75,6 +77,7 @@ export class SpacesGridPage extends Component { } public getPageContent() { + const { intl } = this.props; if (!this.props.userProfile.hasCapability('manageSpaces')) { return ; } @@ -84,7 +87,12 @@ export class SpacesGridPage extends Component { -

Spaces

+

+ +

{this.getPrimaryActionButton()} @@ -99,11 +107,23 @@ export class SpacesGridPage extends Component { pagination={true} search={{ box: { - placeholder: 'Search', + placeholder: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.searchPlaceholder', + defaultMessage: 'Search', + }), }, }} loading={this.state.loading} - message={this.state.loading ? 'loading...' : undefined} + message={ + this.state.loading ? ( + + ) : ( + undefined + ) + } />
); @@ -117,7 +137,10 @@ export class SpacesGridPage extends Component { window.location.hash = `#/management/spaces/create`; }} > - Create space + ); } @@ -145,6 +168,7 @@ export class SpacesGridPage extends Component { }; public deleteSpace = async () => { + const { intl } = this.props; const { spacesManager, spacesNavState } = this.props; const space = this.state.selectedSpace; @@ -158,7 +182,17 @@ export class SpacesGridPage extends Component { } catch (error) { const { message: errorMessage = '' } = error.data || {}; - toastNotifications.addDanger(`Error deleting space: ${errorMessage}`); + toastNotifications.addDanger( + intl.formatMessage( + { + id: 'xpack.spaces.management.spacesGridPage.errorDeletingSpaceErrorMessage', + defaultMessage: 'Error deleting space: {errorMessage}', + }, + { + errorMessage, + } + ) + ); } this.setState({ @@ -167,7 +201,15 @@ export class SpacesGridPage extends Component { this.loadGrid(); - const message = `Deleted "${space.name}" space.`; + const message = intl.formatMessage( + { + id: 'xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage', + defaultMessage: 'Deleted "{spaceName}" space.', + }, + { + spaceName: space.name, + } + ); toastNotifications.addSuccess(message); @@ -203,6 +245,7 @@ export class SpacesGridPage extends Component { }; public getColumnConfig() { + const { intl } = this.props; return [ { field: 'name', @@ -223,7 +266,10 @@ export class SpacesGridPage extends Component { }, { field: 'name', - name: 'Space', + name: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.spaceColumnName', + defaultMessage: 'Space', + }), sortable: true, render: (value: string, record: Space) => { return ( @@ -239,20 +285,35 @@ export class SpacesGridPage extends Component { }, { field: 'id', - name: 'Identifier', + name: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.identifierColumnName', + defaultMessage: 'Identifier', + }), sortable: true, }, { field: 'description', - name: 'Description', + name: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.descriptionColumnName', + defaultMessage: 'Description', + }), sortable: true, }, { - name: 'Actions', + name: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.actionsColumnName', + defaultMessage: 'Actions', + }), actions: [ { - name: 'Edit', - description: 'Edit this space.', + name: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.editSpaceActionName', + defaultMessage: 'Edit', + }), + description: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.editSpaceActionDescription', + defaultMessage: 'Edit this space.', + }), onClick: this.onEditSpaceClick, type: 'icon', icon: 'pencil', @@ -260,8 +321,14 @@ export class SpacesGridPage extends Component { }, { available: (record: Space) => !isReservedSpace(record), - name: 'Delete', - description: 'Delete this space.', + name: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.deleteActionName', + defaultMessage: 'Delete', + }), + description: intl.formatMessage({ + id: 'xpack.spaces.management.spacesGridPage.deleteThisSpaceActionDescription', + defaultMessage: 'Delete this space.', + }), onClick: this.onDeleteSpaceClick, type: 'icon', icon: 'trash', @@ -283,3 +350,5 @@ export class SpacesGridPage extends Component { }); }; } + +export const SpacesGridPage = injectI18n(SpacesGridPageUI); diff --git a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx index 9329f611996c4a8..7bf41e6da33a871 100644 --- a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { UserProfileProvider } from '../../../../../xpack_main/public/services/user_profile'; import { SpaceAvatar } from '../../../components'; import { SpacesManager } from '../../../lib'; @@ -54,22 +54,24 @@ const spacesManager = new SpacesManager(mockHttp, mockChrome, ''); describe('SpacesGridPage', () => { it('renders as expected', () => { expect( - shallow( - ) ).toMatchSnapshot(); }); it('renders the list of spaces', async () => { - const wrapper = mount( - ); diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx index fba09343c179205..322af5670cd64f5 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx @@ -5,6 +5,7 @@ */ import { EuiContextMenuItem, EuiContextMenuPanel, EuiFieldSearch, EuiText } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { UserProfile } from '../../../../../xpack_main/public/services/user_profile'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants'; @@ -16,6 +17,7 @@ interface Props { onSelectSpace: (space: Space) => void; onManageSpacesClick: () => void; userProfile: UserProfile; + intl: InjectedIntl; } interface State { @@ -23,20 +25,24 @@ interface State { allowSpacesListFocus: boolean; } -export class SpacesMenu extends Component { +class SpacesMenuUI extends Component { public state = { searchTerm: '', allowSpacesListFocus: false, }; public render() { + const { intl } = this.props; const { searchTerm } = this.state; const items = this.getVisibleSpaces(searchTerm).map(this.renderSpaceMenuItem); const panelProps = { className: 'spcMenu', - title: 'Change current space', + title: intl.formatMessage({ + id: 'xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle', + defaultMessage: 'Change current space', + }), watchedItemProps: ['data-search-term'], }; @@ -76,8 +82,10 @@ export class SpacesMenu extends Component { if (items.length === 0) { return ( - {' '} - no spaces found{' '} + ); } @@ -95,10 +103,14 @@ export class SpacesMenu extends Component { }; private renderSearchField = () => { + const { intl } = this.props; return (
{ ); }; } + +export const SpacesMenu = injectI18n(SpacesMenuUI); diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx index 5857744d6407c6f..1c03e1a85db95ec 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx @@ -9,6 +9,8 @@ import { SpacesManager } from 'plugins/spaces/lib/spaces_manager'; // @ts-ignore import template from 'plugins/spaces/views/nav_control/nav_control.html'; import { NavControlPopover } from 'plugins/spaces/views/nav_control/nav_control_popover'; +// @ts-ignore +import { PathProvider } from 'plugins/xpack_main/services/path'; import { UserProfileProvider } from 'plugins/xpack_main/services/user_profile'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -45,6 +47,7 @@ module.controller( 'spacesNavController', ($scope: any, $http: any, chrome: any, Private: any, activeSpace: any) => { const userProfile = Private(UserProfileProvider); + const pathProvider = Private(PathProvider); const domNode = document.getElementById(`spacesNavReactRoot`); const spaceSelectorURL = chrome.getInjected('spaceSelectorURL'); @@ -54,7 +57,7 @@ module.controller( let mounted = false; $scope.$parent.$watch('isVisible', function isVisibleWatcher(isVisible: boolean) { - if (isVisible && !mounted) { + if (isVisible && !mounted && !pathProvider.isUnauthenticated()) { render( { ); } else { element = ( - + + + ); } diff --git a/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap b/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap index bc7fa8a00e4d00f..f171ec2e3753837 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap @@ -29,7 +29,11 @@ exports[`it renders without crashing 1`] = ` textTransform="none" >

- Select your space +

- You can change your space at anytime +

@@ -72,7 +80,11 @@ exports[`it renders without crashing 1`] = ` size="m" textAlign="center" > - No spaces match search criteria + diff --git a/x-pack/plugins/spaces/public/views/space_selector/index.tsx b/x-pack/plugins/spaces/public/views/space_selector/index.tsx index 997f88af350e1b0..577b532442c6d97 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/index.tsx +++ b/x-pack/plugins/spaces/public/views/space_selector/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { I18nProvider } from '@kbn/i18n/react'; import { SpacesManager } from 'plugins/spaces/lib/spaces_manager'; // @ts-ignore import template from 'plugins/spaces/views/space_selector/space_selector.html'; @@ -26,7 +27,12 @@ module.controller( const spacesManager = new SpacesManager($http, chrome, spaceSelectorURL); - render(, domNode); + render( + + + , + domNode + ); // unmount react on controller destroy $scope.$on('$destroy', () => { diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx index 96d99f0a293609e..f12b5b8ab8e1c71 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { render, shallow } from 'enzyme'; import React from 'react'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import chrome from 'ui/chrome'; import { Space } from '../../../common/model/space'; import { SpacesManager } from '../../lib/spaces_manager'; @@ -31,7 +31,13 @@ function getSpacesManager(spaces: Space[] = []) { test('it renders without crashing', () => { const spacesManager = getSpacesManager(); - const component = shallow(); + const component = shallowWithIntl( + + ); expect(component).toMatchSnapshot(); }); @@ -46,7 +52,13 @@ test('it uses the spaces on props, when provided', () => { }, ]; - const component = render(); + const component = renderWithIntl( + + ); return Promise.resolve().then(() => { expect(component.find('.spaceCard')).toHaveLength(1); @@ -65,7 +77,9 @@ test('it queries for spaces when not provided on props', () => { const spacesManager = getSpacesManager(spaces); - shallow(); + shallowWithIntl( + + ); return Promise.resolve().then(() => { expect(spacesManager.getSpaces).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.tsx b/x-pack/plugins/spaces/public/views/space_selector/space_selector.tsx index 63dcab7f660c000..3f684c6d42574f4 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.tsx +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.tsx @@ -17,6 +17,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { SpacesManager } from 'plugins/spaces/lib'; import React, { Component, Fragment } from 'react'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common/constants'; @@ -26,6 +27,7 @@ import { SpaceCards } from '../components/space_cards'; interface Props { spaces?: Space[]; spacesManager: SpacesManager; + intl: InjectedIntl; } interface State { @@ -34,7 +36,7 @@ interface State { spaces: Space[]; } -export class SpaceSelector extends Component { +class SpaceSelectorUI extends Component { constructor(props: Props) { super(props); @@ -89,11 +91,22 @@ export class SpaceSelector extends Component { + -

Select your space

+

+ +

-

You can change your space at anytime

+

+ +

@@ -118,7 +131,10 @@ export class SpaceSelector extends Component { // @ts-ignore textAlign="center" > - No spaces match search criteria + )} @@ -129,6 +145,7 @@ export class SpaceSelector extends Component { } public getSearchField = () => { + const { intl } = this.props; if (!this.props.spaces || this.props.spaces.length < SPACE_SEARCH_COUNT_THRESHOLD) { return null; } @@ -136,7 +153,10 @@ export class SpaceSelector extends Component { { this.props.spacesManager.changeSelectedSpace(space); }; } + +export const SpaceSelector = injectI18n(SpaceSelectorUI); diff --git a/x-pack/plugins/watcher/public/services/timezone/xpack_watcher_timezone_service.js b/x-pack/plugins/watcher/public/services/timezone/xpack_watcher_timezone_service.js index 8f57c83082147fe..7f93427fdcc69f0 100644 --- a/x-pack/plugins/watcher/public/services/timezone/xpack_watcher_timezone_service.js +++ b/x-pack/plugins/watcher/public/services/timezone/xpack_watcher_timezone_service.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { jstz as tzDetect } from 'jstimezonedetect'; -import moment from 'moment'; +import moment from 'moment-timezone'; export class XpackWatcherTimezoneService { constructor(config) { @@ -19,7 +18,7 @@ export class XpackWatcherTimezoneService { return this.config.get(DATE_FORMAT_CONFIG_KEY); } - const detectedTimezone = tzDetect.determine().name(); + const detectedTimezone = moment.tz.guess(); if (detectedTimezone) { return detectedTimezone; } diff --git a/x-pack/plugins/xpack_main/public/services/path.js b/x-pack/plugins/xpack_main/public/services/path.js index 3f4a1833fd8ce03..04748fcefdfaf40 100644 --- a/x-pack/plugins/xpack_main/public/services/path.js +++ b/x-pack/plugins/xpack_main/public/services/path.js @@ -10,7 +10,7 @@ export function PathProvider($window) { const path = chrome.removeBasePath($window.location.pathname); return { isUnauthenticated() { - return path === '/login' || path === '/logout' || path === '/logged_out'; + return path === '/login' || path === '/logout' || path === '/logged_out' || path === '/status'; } }; } diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index aa2273ce642120f..41d4cabc3b63f61 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -5,7 +5,9 @@ */ export default function ({ loadTestFile }) { - describe('apis', () => { + describe('apis', function () { + this.tags('ciGroup5'); + loadTestFile(require.resolve('./es')); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./monitoring')); diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js index aadef1c18b8f8eb..29074b31aa3e393 100644 --- a/x-pack/test/api_integration/apis/infra/index.js +++ b/x-pack/test/api_integration/apis/infra/index.js @@ -7,7 +7,7 @@ export default function ({ loadTestFile }) { describe('InfraOps GraphQL Endpoints', () => { - loadTestFile(require.resolve('./capabilities')); + loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./log_entries')); loadTestFile(require.resolve('./log_summary')); loadTestFile(require.resolve('./metrics')); diff --git a/x-pack/test/api_integration/apis/infra/capabilities.ts b/x-pack/test/api_integration/apis/infra/metadata.ts similarity index 57% rename from x-pack/test/api_integration/apis/infra/capabilities.ts rename to x-pack/test/api_integration/apis/infra/metadata.ts index 56b3349b8f94394..741a2268803ab25 100644 --- a/x-pack/test/api_integration/apis/infra/capabilities.ts +++ b/x-pack/test/api_integration/apis/infra/metadata.ts @@ -5,22 +5,22 @@ */ import expect from 'expect.js'; -import { CapabilitiesQuery } from '../../../../plugins/infra/common/graphql/types'; -import { capabilitiesQuery } from '../../../../plugins/infra/public/containers/capabilities/capabilities.gql_query'; +import { MetadataQuery } from '../../../../plugins/infra/common/graphql/types'; +import { metadataQuery } from '../../../../plugins/infra/public/containers/metadata/metadata.gql_query'; import { KbnTestProvider } from './types'; -const capabilitiesTests: KbnTestProvider = ({ getService }) => { +const metadataTests: KbnTestProvider = ({ getService }) => { const esArchiver = getService('esArchiver'); const client = getService('infraOpsGraphQLClient'); - describe('capabilities', () => { + describe('metadata', () => { before(() => esArchiver.load('infra')); after(() => esArchiver.unload('infra')); - it('supports the capabilities container query', () => { + it('supports the metadata container query', () => { return client - .query({ - query: capabilitiesQuery, + .query({ + query: metadataQuery, variables: { sourceId: 'default', nodeId: 'demo-stack-nginx-01', @@ -28,12 +28,12 @@ const capabilitiesTests: KbnTestProvider = ({ getService }) => { }, }) .then(resp => { - const capabilities = resp.data.source.capabilitiesByNode; - expect(capabilities.length).to.be(14); + const metadata = resp.data.source.metadataByNode; + expect(metadata.length).to.be(14); }); }); }); }; // tslint:disable-next-line no-default-export -export default capabilitiesTests; +export default metadataTests; diff --git a/x-pack/test/functional/apps/dashboard_mode/index.js b/x-pack/test/functional/apps/dashboard_mode/index.js index e7907f4fa6508ae..5953dd4924c5d48 100644 --- a/x-pack/test/functional/apps/dashboard_mode/index.js +++ b/x-pack/test/functional/apps/dashboard_mode/index.js @@ -6,6 +6,8 @@ export default function ({ loadTestFile }) { describe('dashboard mode', function () { + this.tags('ciGroup3'); + loadTestFile(require.resolve('./dashboard_view_mode')); }); } diff --git a/x-pack/test/functional/apps/graph/index.js b/x-pack/test/functional/apps/graph/index.js index d77ec07969e6354..98b360320c2c79e 100644 --- a/x-pack/test/functional/apps/graph/index.js +++ b/x-pack/test/functional/apps/graph/index.js @@ -6,6 +6,8 @@ export default function ({ loadTestFile }) { describe('graph app', function () { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./graph')); }); } diff --git a/x-pack/test/functional/apps/grok_debugger/index.js b/x-pack/test/functional/apps/grok_debugger/index.js index 3dc15acbe5e30d3..75c05f35abd289f 100644 --- a/x-pack/test/functional/apps/grok_debugger/index.js +++ b/x-pack/test/functional/apps/grok_debugger/index.js @@ -5,7 +5,9 @@ */ export default function ({ loadTestFile }) { - describe('logstash', () => { + describe('logstash', function () { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./grok_debugger')); }); } diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index d7efdb85838df96..04382e4964cf0b8 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -15,6 +15,10 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi const pageObjects = getPageObjects(['common', 'infraHome']); describe('Home page', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + describe('without metrics present', () => { before(async () => await esArchiver.unload('infra')); diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index efb3cc9f276bc68..87abb2584b6c293 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -8,7 +8,9 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // tslint:disable-next-line:no-default-export export default ({ loadTestFile }: KibanaFunctionalTestDefaultProviders) => { - describe('InfraOps app', () => { + describe('InfraOps app', function() { + (this as any).tags('ciGroup4'); + loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index bd7577e2b88f7e3..bc7d2941b7198c1 100644 --- a/x-pack/test/functional/apps/logstash/index.js +++ b/x-pack/test/functional/apps/logstash/index.js @@ -5,7 +5,9 @@ */ export default function ({ loadTestFile }) { - describe('logstash', () => { + describe('logstash', function () { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./pipeline_list')); loadTestFile(require.resolve('./pipeline_create')); }); diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 817467f847ddc45..a1551f2829a98a2 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -5,7 +5,9 @@ */ export default function ({ loadTestFile }) { - describe('Monitoring app', () => { + describe('Monitoring app', function () { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./cluster/list')); loadTestFile(require.resolve('./cluster/overview')); loadTestFile(require.resolve('./cluster/alerts')); diff --git a/x-pack/test/functional/apps/security/index.js b/x-pack/test/functional/apps/security/index.js index 0bccef48960c341..2de59ddae3d3ca0 100644 --- a/x-pack/test/functional/apps/security/index.js +++ b/x-pack/test/functional/apps/security/index.js @@ -6,6 +6,8 @@ export default function ({ loadTestFile }) { describe('security app', function () { + this.tags('ciGroup4'); + loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./doc_level_security_roles')); loadTestFile(require.resolve('./management')); diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index 455692b86fc81f3..76eb7d911b17b41 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -8,6 +8,8 @@ import { TestInvoker } from './lib/types'; // tslint:disable:no-default-export export default function spacesApp({ loadTestFile }: TestInvoker) { describe('Spaces app', function spacesAppTestSuite() { + (this as any).tags('ciGroup4'); + loadTestFile(require.resolve('./spaces_selection')); }); } diff --git a/x-pack/test/functional/apps/status_page/index.ts b/x-pack/test/functional/apps/status_page/index.ts new file mode 100644 index 000000000000000..6b6d5c1d9760048 --- /dev/null +++ b/x-pack/test/functional/apps/status_page/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TestInvoker } from './lib/types'; + +// tslint:disable:no-default-export +export default function statusPage({ loadTestFile }: TestInvoker) { + describe('Status page', function statusPageTestSuite() { + (this as any).tags('ciGroup4'); + + loadTestFile(require.resolve('./status_page')); + }); +} diff --git a/x-pack/test/functional/apps/status_page/lib/types.ts b/x-pack/test/functional/apps/status_page/lib/types.ts new file mode 100644 index 000000000000000..2ed91406e5f48d2 --- /dev/null +++ b/x-pack/test/functional/apps/status_page/lib/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type DescribeFn = (text: string, fn: () => void) => void; + +export interface TestDefinitionAuthentication { + username?: string; + password?: string; +} + +export type LoadTestFileFn = (path: string) => string; + +export type GetServiceFn = (service: string) => any; + +export type ReadConfigFileFn = (path: string) => any; + +export type GetPageObjectsFn = (pageObjects: string[]) => any; + +export interface TestInvoker { + getService: GetServiceFn; + getPageObjects: GetPageObjectsFn; + loadTestFile: LoadTestFileFn; + readConfigFile: ReadConfigFileFn; +} diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts new file mode 100644 index 000000000000000..d72d3b55522aca0 --- /dev/null +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -0,0 +1,22 @@ +/* + * 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 { TestInvoker } from './lib/types'; + +// tslint:disable:no-default-export +export default function statusPageFunctonalTests({ getService, getPageObjects }: TestInvoker) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['security', 'statusPage', 'home']); + + describe('Status Page', () => { + before(async () => await esArchiver.load('empty_kibana')); + after(async () => await esArchiver.unload('empty_kibana')); + + it('allows user to navigate without authentication', async () => { + await PageObjects.statusPage.navigateToPage(); + await PageObjects.statusPage.expectStatusPage(); + }); + }); +} diff --git a/x-pack/test/functional/apps/watcher/index.js b/x-pack/test/functional/apps/watcher/index.js index 80e60c213d21161..5542f5cb8f0b714 100644 --- a/x-pack/test/functional/apps/watcher/index.js +++ b/x-pack/test/functional/apps/watcher/index.js @@ -6,6 +6,8 @@ export default function ({ loadTestFile }) { describe('watcher app', function () { + this.tags('ciGroup1'); + //loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./watcher_test')); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index ac13f32bc0a2b38..5183461a4760ede 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -19,6 +19,7 @@ import { SpaceSelectorPageProvider, AccountSettingProvider, InfraHomePageProvider, + StatusPagePageProvider, } from './page_objects'; import { @@ -71,6 +72,7 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/logstash'), resolve(__dirname, './apps/grok_debugger'), resolve(__dirname, './apps/infra'), + resolve(__dirname, './apps/status_page'), ], // define the name and providers for services that should be @@ -121,6 +123,7 @@ export default async function ({ readConfigFile }) { reporting: ReportingPageProvider, spaceSelector: SpaceSelectorPageProvider, infraHome: InfraHomePageProvider, + statusPage: StatusPagePageProvider, }, servers: kibanaFunctionalConfig.get('servers'), @@ -138,6 +141,7 @@ export default async function ({ readConfigFile }) { ...kibanaCommonConfig.get('kbnTestServer'), serverArgs: [ ...kibanaCommonConfig.get('kbnTestServer.serverArgs'), + '--status.allowAnonymous=true', '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', '--xpack.xpack_main.telemetry.enabled=false', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions diff --git a/x-pack/test/functional/page_objects/index.js b/x-pack/test/functional/page_objects/index.js index d6654fec9f9be78..c21d37ebb09bedf 100644 --- a/x-pack/test/functional/page_objects/index.js +++ b/x-pack/test/functional/page_objects/index.js @@ -14,3 +14,4 @@ export { ReportingPageProvider } from './reporting_page'; export { SpaceSelectorPageProvider } from './space_selector_page'; export { AccountSettingProvider } from './accountsetting_page'; export { InfraHomePageProvider } from './infra_home_page'; +export { StatusPagePageProvider } from './status_page'; diff --git a/x-pack/test/functional/page_objects/status_page.js b/x-pack/test/functional/page_objects/status_page.js new file mode 100644 index 000000000000000..b56423b417d764e --- /dev/null +++ b/x-pack/test/functional/page_objects/status_page.js @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; + +export function StatusPagePageProvider({ getService, getPageObjects }) { + const retry = getService('retry'); + const log = getService('log'); + const remote = getService('remote'); + const PageObjects = getPageObjects(['common', 'home', 'security']); + + class StatusPage { + async initTests() { + log.debug('StatusPage:initTests'); + } + + async navigateToPage() { + return await retry.try(async () => { + const url = PageObjects.common.getHostPort() + '/status'; + log.info(`StatusPage:navigateToPage(): ${url}`); + await remote.get(url); + }); + } + + async expectStatusPage() { + return await retry.try(async () => { + log.debug(`expectStatusPage()`); + await remote.setFindTimeout(20000).findByCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) '); + const url = await remote.getCurrentUrl(); + expect(url).to.contain(`/status`); + }); + } + } + + return new StatusPage(); +} diff --git a/x-pack/test/reporting/api/chromium_tests.js b/x-pack/test/reporting/api/chromium_tests.js index d9da4ac802cbccf..c9282eda4f0cc2b 100644 --- a/x-pack/test/reporting/api/chromium_tests.js +++ b/x-pack/test/reporting/api/chromium_tests.js @@ -10,7 +10,9 @@ export default function ({ loadTestFile, getService }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - describe('chromium', () => { + describe('chromium', function () { + this.tags('ciGroup6'); + before(async () => { await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); await esArchiver.load(OSS_DATA_ARCHIVE_PATH); diff --git a/x-pack/test/reporting/api/phantom_tests.js b/x-pack/test/reporting/api/phantom_tests.js index 4a3212bff46ffa2..e0c8c11b326bd53 100644 --- a/x-pack/test/reporting/api/phantom_tests.js +++ b/x-pack/test/reporting/api/phantom_tests.js @@ -10,7 +10,9 @@ export default function ({ loadTestFile, getService }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - describe('phantom', () => { + describe('phantom', function () { + this.tags('ciGroup6'); + before(async () => { await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); await esArchiver.load(OSS_DATA_ARCHIVE_PATH); diff --git a/x-pack/test/reporting/configs/chromium_functional.js b/x-pack/test/reporting/configs/chromium_functional.js index f14d943746645d9..80a9024c06ab8b7 100644 --- a/x-pack/test/reporting/configs/chromium_functional.js +++ b/x-pack/test/reporting/configs/chromium_functional.js @@ -13,7 +13,7 @@ export default async function ({ readConfigFile }) { return { ...functionalConfig, junit: { - reportName: 'X-Pack Chromium API Reporting Tests', + reportName: 'X-Pack Chromium Functional Reporting Tests', }, testFiles: [require.resolve('../functional')], kbnTestServer: { diff --git a/x-pack/test/reporting/configs/phantom_functional.js b/x-pack/test/reporting/configs/phantom_functional.js index 48d2559479186a1..e9a61ee813cf2d6 100644 --- a/x-pack/test/reporting/configs/phantom_functional.js +++ b/x-pack/test/reporting/configs/phantom_functional.js @@ -13,7 +13,7 @@ export default async function ({ readConfigFile }) { return { ...functionalConfig, junit: { - reportName: 'X-Pack Phantom API Reporting Tests', + reportName: 'X-Pack Phantom Functional Reporting Tests', }, testFiles: [require.resolve('../functional')], kbnTestServer: { diff --git a/x-pack/test/reporting/functional/index.js b/x-pack/test/reporting/functional/index.js index d99cf9a900153bd..fa473f454a9258b 100644 --- a/x-pack/test/reporting/functional/index.js +++ b/x-pack/test/reporting/functional/index.js @@ -6,6 +6,7 @@ export default function ({ loadTestFile }) { describe('reporting app', function () { + this.tags('ciGroup6'); loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/saml_api_integration/apis/index.js b/x-pack/test/saml_api_integration/apis/index.js index ce7c48029e66afb..ac08d2e078abfdd 100644 --- a/x-pack/test/saml_api_integration/apis/index.js +++ b/x-pack/test/saml_api_integration/apis/index.js @@ -5,7 +5,8 @@ */ export default function ({ loadTestFile }) { - describe('apis SAML', () => { + describe('apis SAML', function () { + this.tags('ciGroup6'); loadTestFile(require.resolve('./security')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index d25a9b852b789d4..b2b62b501e7654a 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -12,7 +12,9 @@ export default function({ getService, loadTestFile }: TestInvoker) { const es = getService('es'); const supertest = getService('supertest'); - describe('saved objects security and spaces enabled', () => { + describe('saved objects security and spaces enabled', function() { + (this as any).tags('ciGroup5'); + before(async () => { await createUsersAndRoles(es, supertest); }); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts index c9be7152f96eae2..a0797300ebf2b5b 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts @@ -12,7 +12,9 @@ export default function({ getService, loadTestFile }: TestInvoker) { const es = getService('es'); const supertest = getService('supertest'); - describe('saved objects security only enabled', () => { + describe('saved objects security only enabled', function() { + (this as any).tags('ciGroup5'); + before(async () => { await createUsersAndRoles(es, supertest); }); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts index 113cf86454d5ffa..b1028fa6c74c946 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts @@ -8,7 +8,9 @@ import { TestInvoker } from '../../common/lib/types'; // tslint:disable:no-default-export export default function({ loadTestFile }: TestInvoker) { - describe('saved objects spaces only enabled', () => { + describe('saved objects spaces only enabled', function() { + (this as any).tags('ciGroup5'); + loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); loadTestFile(require.resolve('./create')); diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index 65ab34e7ae8f811..6d8402fbb924407 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -12,7 +12,9 @@ export default function({ loadTestFile, getService }: TestInvoker) { const es = getService('es'); const supertest = getService('supertest'); - describe('spaces api with security', () => { + describe('spaces api with security', function() { + (this as any).tags('ciGroup5'); + before(async () => { await createUsersAndRoles(es, supertest); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 6864ee7fbda9468..75b546dd1602291 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -8,7 +8,9 @@ import { TestInvoker } from '../../common/lib/types'; // tslint:disable:no-default-export export default function spacesOnlyTestSuite({ loadTestFile }: TestInvoker) { - describe('spaces api without security', () => { + describe('spaces api without security', function() { + (this as any).tags('ciGroup5'); + loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 0f4b296e666b66a..4f7c9e3e8b275e9 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,6 +22,9 @@ ], "plugins/spaces/*": [ "x-pack/plugins/spaces/public/*" + ], + "test_utils/*": [ + "x-pack/test_utils/*" ] }, "types": [ diff --git a/yarn.lock b/yarn.lock index 3b32db6bd53dbfe..dd53f0adf14e5b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10624,6 +10624,11 @@ highlight.js@^9.12.0, highlight.js@~9.12.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" integrity sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4= +history-extra@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-4.0.2.tgz#67b512c196e0a521be1d8ac83513f00ca761d9f3" + integrity sha512-Nia8vzQHyQcxKQUt5/vxZegurkG2K23TplaWLk1V6EWTuNdZMJUJ78anwGkBcHuLBia8TFUct/R/QDlgRUA42A== + history@4.7.2, history@^4.7.2: version "4.7.2" resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" @@ -10896,15 +10901,6 @@ humps@2.0.1: resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao= -husky@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" - integrity sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA== - dependencies: - is-ci "^1.0.10" - normalize-path "^1.0.0" - strip-indent "^2.0.0" - icalendar@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" @@ -11277,6 +11273,11 @@ intl-relativeformat@^2.1.0: dependencies: intl-messageformat "^2.0.0" +intl@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" + integrity sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94= + into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -12746,11 +12747,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jstimezonedetect@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/jstimezonedetect/-/jstimezonedetect-1.0.5.tgz#93d035cd20e8c7d64eb1375cf5aa7a10a024466a" - integrity sha1-k9A1zSDox9ZOsTdc9ap6EKAkRmo= - jstransformer-ejs@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/jstransformer-ejs/-/jstransformer-ejs-0.0.3.tgz#04d9201469274fcf260f1e7efd732d487fa234b6" @@ -15091,11 +15087,6 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" - integrity sha1-MtDkcvkf80VwHBWoMRAY07CpA3k= - normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"