diff --git a/.ci/build_docker.sh b/.ci/build_docker.sh new file mode 100755 index 0000000000000..1f45182aad840 --- /dev/null +++ b/.ci/build_docker.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "${0}")" + +cp /usr/local/bin/runbld ./ +cp /usr/local/bin/bash_standard_lib.sh ./ + +docker build -t kibana-ci -f ./Dockerfile . diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh new file mode 100755 index 0000000000000..0d9b22b04dbd0 --- /dev/null +++ b/.ci/packer_cache_for_branch.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -e + +branch="$1" +checkoutDir="$(pwd)" + +if [[ "$branch" != "master" ]]; then + checkoutDir="/tmp/kibana-$branch" + git clone https://github.com/elastic/kibana.git --branch "$branch" --depth 1 "$checkoutDir" + cd "$checkoutDir" +fi + +source src/dev/ci_setup/setup.sh; + +# download es snapshots +node scripts/es snapshot --download-only; +node scripts/es snapshot --license=oss --download-only; + +# download reporting browsers +(cd "x-pack" && node ../node_modules/.bin/gulp downloadChromium); + +# cache the chromedriver archive +chromedriverDistVersion="$(node -e "console.log(require('chromedriver').version)")" +chromedriverPkgVersion="$(node -e "console.log(require('./package.json').devDependencies.chromedriver)")" +if [ -z "$chromedriverDistVersion" ] || [ -z "$chromedriverPkgVersion" ]; then + echo "UNABLE TO DETERMINE CHROMEDRIVER VERSIONS" + exit 1 +fi +mkdir -p .chromedriver +curl "https://chromedriver.storage.googleapis.com/$chromedriverDistVersion/chromedriver_linux64.zip" > .chromedriver/chromedriver.zip +echo "$chromedriverPkgVersion" > .chromedriver/pkgVersion + +# cache the geckodriver archive +geckodriverPkgVersion="$(node -e "console.log(require('./package.json').devDependencies.geckodriver)")" +if [ -z "$geckodriverPkgVersion" ]; then + echo "UNABLE TO DETERMINE geckodriver VERSIONS" + exit 1 +fi +mkdir -p ".geckodriver" +cp "node_modules/geckodriver/geckodriver.tar.gz" .geckodriver/geckodriver.tar.gz +echo "$geckodriverPkgVersion" > .geckodriver/pkgVersion + +echo "Creating bootstrap_cache archive" + +# archive cacheable directories +mkdir -p "$HOME/.kibana/bootstrap_cache" +tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ + .chromium \ + .es \ + .chromedriver \ + .geckodriver; + +echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" + +.ci/build_docker.sh + +if [[ "$branch" != "master" ]]; then + rm --preserve-root -rf "$checkoutDir" +fi diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index 35f1160ee834d..c1d287fca1f44 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -147,7 +147,7 @@ To match multiple fields: machine.os*:windows 10 ------------------- -This sytax is handy when you have text and keyword +This syntax is handy when you have text and keyword versions of a field. The query checks machine.os and machine.os.keyword for the term `windows 10`. diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 3720a5b457d84..75c6fddb484ac 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -74,6 +74,9 @@ status codes, you could enter `status:[400 TO 499]`. codes and have an extension of `php` or `html`, you could enter `status:[400 TO 499] AND (extension:php OR extension:html)`. +IMPORTANT: When you use the Lucene Query Syntax in the *KQL* search bar, {kib} is unable to search on nested objects and perform aggregations across fields that contain nested objects. +Using `include_in_parent` or `copy_to` as a workaround can cause {kib} to fail. + For more detailed information about the Lucene query syntax, see the {ref}/query-dsl-query-string-query.html#query-string-syntax[Query String Query] docs. diff --git a/docs/index.asciidoc b/docs/index.asciidoc index f4d2b7d066f12..0147a66704de8 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -21,8 +21,6 @@ include::user/index.asciidoc[] include::accessibility.asciidoc[] -include::limitations.asciidoc[] - include::migration.asciidoc[] include::CHANGELOG.asciidoc[] diff --git a/docs/limitations.asciidoc b/docs/limitations.asciidoc index 30a716641cc5d..97d3bd9d4f73c 100644 --- a/docs/limitations.asciidoc +++ b/docs/limitations.asciidoc @@ -4,12 +4,6 @@ Following are the known limitations in {kib}. -[float] -=== Exporting data - -Exporting a data table or saved search from a dashboard or visualization report -has known limitations. The PDF report only includes the data visible on the screen. - [float] === Nested objects @@ -22,17 +16,3 @@ the query bar. Using `include_in_parent` or `copy_to` as a workaround is not supported and may stop functioning in future releases. ============================================== -[float] -=== Graph - -Graph has limited support for multiple indices. -Go to <> for details. - -[float] -=== Other limitations - -These {stack} features have limitations that affect {kib}: - -* {ref}/watcher-limitations.html[Alerting] -* {ml-docs}/ml-limitations.html[Machine learning] -* {ref}/security-limitations.html[Security] diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index aded7a45022db..0bc6365918866 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -27,6 +27,8 @@ threshold watch, take a look at the different watcher actions. If you are creating an advanced watch, you should be familiar with the parts of a watch—input, schedule, condition, and actions. +NOTE: There are limitations in *Watcher* that affect {kib}. For information, refer to {ref}/watcher-limitations.html[Alerting]. + [float] [[watcher-security]] === Watcher security diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc deleted file mode 100644 index 7b24de42d8e1c..0000000000000 --- a/docs/plugins/known-plugins.asciidoc +++ /dev/null @@ -1,74 +0,0 @@ -[[known-plugins]] -== Known Plugins - -[IMPORTANT] -.Plugin compatibility -============================================== -The Kibana plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. Kibana enforces that the installed plugins match the version of Kibana itself. Plugin developers will have to release a new version of their plugin for each new Kibana release as a result. -============================================== - -This list of plugins is not guaranteed to work on your version of Kibana. Instead, these are plugins that were known to work at some point with Kibana *5.x*. The Kibana installer will reject any plugins that haven't been published for your specific version of Kibana. These plugins are not evaluated or maintained by Elastic, so care should be taken before installing them into your environment. - -[float] -=== Apps -* https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface -* https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy -* https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation -* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. -* https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. -* https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API -* https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. -* https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules -* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights - -[float] -=== Timelion Extensions -* https://github.com/fermiumlabs/mathlion[mathlion] (fermiumlabs) - enables equation parsing and advanced math under Timelion - -[float] -=== Visualizations -* https://github.com/virusu/3D_kibana_charts_vis[3D Charts] (virusu) -* https://github.com/JuanCarniglia/area3d_vis[3D Graph] (JuanCarniglia) -* https://github.com/TrumanDu/bmap[Bmap](TrumanDu) - integrated echarts for map visualization -* https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano) -* https://github.com/aaronoah/kibana_calendar_vis[Calendar Visualization] (aaronoah) -* https://github.com/elo7/cohort[Cohort analysis] (elo7) -* https://github.com/DeanF/health_metric_vis[Colored Metric Visualization] (deanf) -* https://github.com/JuanCarniglia/dendrogram_vis[Dendrogram] (JuanCarniglia) -* https://github.com/dlumbrer/kbn_dotplot[Dotplot] (dlumbrer) -* https://github.com/AnnaGerber/kibana_dropdown[Dropdown] (AnnaGerber) -* https://github.com/fbaligand/kibana-enhanced-table[Enhanced Table] (fbaligand) -* https://github.com/nreese/enhanced_tilemap[Enhanced Tilemap] (nreese) -* https://github.com/ommsolutions/kibana_ext_metrics_vis[Extended Metric] (ommsolutions) -* https://github.com/flexmonster/pivot-kibana[Flexmonster Pivot Table & Charts] - a customizable pivot table component for advanced data analysis and reporting. -* https://github.com/outbrain/ob-kb-funnel[Funnel Visualization] (roybass) -* https://github.com/sbeyn/kibana-plugin-gauge-sg[Gauge] (sbeyn) -* https://github.com/clamarque/Kibana_health_metric_vis[Health Metric] (clamarque) -* https://github.com/tshoeb/Insight[Insight] (tshoeb) - Multidimensional data exploration -* https://github.com/sbeyn/kibana-plugin-line-sg[Line] (sbeyn) -* https://github.com/walterra/kibana-milestones-vis[Milestones] (walterra) -* https://github.com/varundbest/navigation[Navigation] (varundbest) -* https://github.com/dlumbrer/kbn_network[Network Plugin] (dlumbrer) -* https://github.com/amannocci/kibana-plugin-metric-percent[Percent] (amannocci) -* https://github.com/dlumbrer/kbn_polar[Polar] (dlumbrer) -* https://github.com/dlumbrer/kbn_radar[Radar] (dlumbrer) -* https://github.com/dlumbrer/kbn_searchtables[Search-Tables] (dlumbrer) -* https://github.com/Smeds/status_light_visualization[Status Light] (smeds) -* https://github.com/prelert/kibana-swimlane-vis[Swimlanes] (prelert) -* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn) -* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek) -* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity. -* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan) - -[float] -=== Other -* https://github.com/nreese/kibana-time-plugin[Time filter as a dashboard panel] Widget to view and edit the time range from within dashboards. - -* https://github.com/Webiks/kibana-API.git[Kibana-API] (webiks) Exposes an API with Kibana functionality. -Use it to create, edit and embed visualizations, and also to search inside an embedded dashboard. - -* https://github.com/sw-jung/kibana_markdown_doc_view[Markdown Doc View] (sw-jung) - A plugin for custom doc view using markdown+handlebars template. -* https://github.com/datasweet-fr/kibana-datasweet-formula[Datasweet Formula] (datasweet) - enables calculated metric on any standard Kibana visualization. -* https://github.com/pjhampton/kibana-prometheus-exporter[Prometheus Exporter] - exports the Kibana metrics in the prometheus format - -NOTE: If you want your plugin to be added to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index 8255585aae411..fa15e0652e2ab 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -26,6 +26,8 @@ If {stack-security-features} are enabled, users must have the necessary privileges to use {ml-features}. Refer to {ml-docs}/setup.html#setup-privileges[Set up {ml-features}]. +NOTE: There are limitations in {ml-features} that affect {kib}. For more information, refer to {ml-docs}/ml-limitations.html[Machine learning]. + -- [[xpack-ml-anomalies]] diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc index a96fe811dc84f..fa9e7d0c513b5 100644 --- a/docs/user/plugins.asciidoc +++ b/docs/user/plugins.asciidoc @@ -1,20 +1,90 @@ +[chapter] [[kibana-plugins]] -= Kibana plugins += {kib} plugins -[partintro] --- -Add-on functionality for {kib} is implemented with plug-in modules. You use the `bin/kibana-plugin` -command to manage these modules. +Implement add-on functionality for {kib} with plug-in modules. [IMPORTANT] .Plugin compatibility ============================================== -The {kib} plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. {kib} enforces that the installed plugins match the version of {kib} itself. Plugin developers will have to release a new version of their plugin for each new {kib} release as a result. +The {kib} plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. {kib} enforces that the installed plugins match the version of {kib}. +Plugin developers must release a new version of their plugin for each new {kib} release. ============================================== --- +[float] +[[known-plugins]] +== Known plugins + +The known plugins were tested for {kib} *5.x*, so we are unable to guarantee compatibility with your version of {kib}. The {kib} installer rejects any plugins that haven't been published for your specific version of {kib}. +We are unable to evaluate or maintain the known plugins, so care should be taken before installation. + +[float] +=== Apps +* https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface +* https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy +* https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation +* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. +* https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. +* https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API +* https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. +* https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules +* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights + +[float] +=== Timelion Extensions +* https://github.com/fermiumlabs/mathlion[mathlion] (fermiumlabs) - enables equation parsing and advanced math under Timelion + +[float] +=== Visualizations +* https://github.com/virusu/3D_kibana_charts_vis[3D Charts] (virusu) +* https://github.com/JuanCarniglia/area3d_vis[3D Graph] (JuanCarniglia) +* https://github.com/TrumanDu/bmap[Bmap](TrumanDu) - integrated echarts for map visualization +* https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano) +* https://github.com/aaronoah/kibana_calendar_vis[Calendar Visualization] (aaronoah) +* https://github.com/elo7/cohort[Cohort analysis] (elo7) +* https://github.com/DeanF/health_metric_vis[Colored Metric Visualization] (deanf) +* https://github.com/JuanCarniglia/dendrogram_vis[Dendrogram] (JuanCarniglia) +* https://github.com/dlumbrer/kbn_dotplot[Dotplot] (dlumbrer) +* https://github.com/AnnaGerber/kibana_dropdown[Dropdown] (AnnaGerber) +* https://github.com/fbaligand/kibana-enhanced-table[Enhanced Table] (fbaligand) +* https://github.com/nreese/enhanced_tilemap[Enhanced Tilemap] (nreese) +* https://github.com/ommsolutions/kibana_ext_metrics_vis[Extended Metric] (ommsolutions) +* https://github.com/flexmonster/pivot-kibana[Flexmonster Pivot Table & Charts] - a customizable pivot table component for advanced data analysis and reporting. +* https://github.com/outbrain/ob-kb-funnel[Funnel Visualization] (roybass) +* https://github.com/sbeyn/kibana-plugin-gauge-sg[Gauge] (sbeyn) +* https://github.com/clamarque/Kibana_health_metric_vis[Health Metric] (clamarque) +* https://github.com/tshoeb/Insight[Insight] (tshoeb) - Multidimensional data exploration +* https://github.com/sbeyn/kibana-plugin-line-sg[Line] (sbeyn) +* https://github.com/walterra/kibana-milestones-vis[Milestones] (walterra) +* https://github.com/varundbest/navigation[Navigation] (varundbest) +* https://github.com/dlumbrer/kbn_network[Network Plugin] (dlumbrer) +* https://github.com/amannocci/kibana-plugin-metric-percent[Percent] (amannocci) +* https://github.com/dlumbrer/kbn_polar[Polar] (dlumbrer) +* https://github.com/dlumbrer/kbn_radar[Radar] (dlumbrer) +* https://github.com/dlumbrer/kbn_searchtables[Search-Tables] (dlumbrer) +* https://github.com/Smeds/status_light_visualization[Status Light] (smeds) +* https://github.com/prelert/kibana-swimlane-vis[Swimlanes] (prelert) +* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn) +* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek) +* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity. +* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan) + +[float] +=== Other +* https://github.com/nreese/kibana-time-plugin[Time filter as a dashboard panel] Widget to view and edit the time range from within dashboards. + +* https://github.com/Webiks/kibana-API.git[Kibana-API] (webiks) Exposes an API with Kibana functionality. +Use it to create, edit and embed visualizations, and also to search inside an embedded dashboard. + +* https://github.com/sw-jung/kibana_markdown_doc_view[Markdown Doc View] (sw-jung) - A plugin for custom doc view using markdown+handlebars template. +* https://github.com/datasweet-fr/kibana-datasweet-formula[Datasweet Formula] (datasweet) - enables calculated metric on any standard Kibana visualization. +* https://github.com/pjhampton/kibana-prometheus-exporter[Prometheus Exporter] - exports the Kibana metrics in the prometheus format + +NOTE: To add your plugin to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. + +[float] [[install-plugin]] == Install plugins @@ -60,6 +130,7 @@ You can specify the environment variable directly when installing plugins: [source,shell] $ http_proxy="http://proxy.local:4242" bin/kibana-plugin install +[float] [[update-remove-plugin]] == Update and remove plugins @@ -74,6 +145,7 @@ You can also remove a plugin manually by deleting the plugin's subdirectory unde NOTE: Removing a plugin will result in an "optimize" run which will delay the next start of {kib}. +[float] [[disable-plugin]] == Disable plugins @@ -88,6 +160,7 @@ NOTE: Disabling or enabling a plugin will result in an "optimize" run which will <1> You can find a plugin's plugin ID as the value of the `name` property in the plugin's `package.json` file. +[float] [[configure-plugin-manager]] == Configure the plugin manager @@ -125,5 +198,3 @@ you must specify the path to that configuration file each time you use the `bin/ 64:: Unknown command or incorrect option parameter 74:: I/O error 70:: Other error - -include::{kib-repo-dir}/plugins/known-plugins.asciidoc[] diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index cd93389bb5fde..224973d3c840c 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -55,6 +55,8 @@ click the share icon image:user/reporting/images/canvas-share-button.png["Canvas + A notification appears when the report is complete. +NOTE: When you export a data table or saved search from a dashboard report, the PDF includes only the visible data. + [float] [[reporting-layout-sizing]] == Layout and sizing diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index 18ace452ce00c..f84e9de87c734 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -10,6 +10,8 @@ auditing. For more information, see {ref}/secure-cluster.html[Secure a cluster] and <>. +NOTE: There are security limitations that affect {kib}. For more information, refer to {ref}/security-limitations.html[Security]. + [float] === Required permissions diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 2eca8d1deedcc..dd3d9c29f19c9 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -373,12 +373,7 @@ def scriptTaskDocker(description, script) { def buildDocker() { sh( - script: """ - cp /usr/local/bin/runbld .ci/ - cp /usr/local/bin/bash_standard_lib.sh .ci/ - cd .ci - docker build -t kibana-ci -f ./Dockerfile . - """, + script: "./.ci/build_docker.sh", label: 'Build CI Docker image' ) } diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 5be8ad141ffd0..849dd7f5c3e2d 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -29,7 +29,7 @@ module.exports = { roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], collectCoverage: true, collectCoverageFrom: [ - ...jestConfig.collectCoverageFrom, + ...(jestConfig.collectCoverageFrom || []), '**/*.{js,mjs,jsx,ts,tsx}', '!**/*.stories.{js,mjs,ts,tsx}', '!**/dev_docs/**', diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/Documentation.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/Documentation.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FiltersSection.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FiltersSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FlyoutFooter.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FlyoutFooter.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkSection.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts similarity index 99% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts index 5f8e0b9052a65..4af9321152da3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts @@ -6,7 +6,7 @@ import { getSelectOptions, replaceTemplateVariables, -} from '../CustomLinkFlyout/helper'; +} from '../CreateEditCustomLinkFlyout/helper'; import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; describe('Custom link helper', () => { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx index 9687846d6c520..c6566af3a8b61 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx @@ -37,7 +37,7 @@ interface Props { const filtersEmptyState: Filter[] = [{ key: '', value: '' }]; -export function CustomLinkFlyout({ +export function CreateEditCustomLinkFlyout({ onClose, onSave, onDelete, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx index 4ccfc5b3013e9..cd8455f0e29a1 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { LinkPreview } from '../CustomLinkFlyout/LinkPreview'; +import { LinkPreview } from '../CreateEditCustomLinkFlyout/LinkPreview'; import { render, getNodeText, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/saveCustomLink.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/saveCustomLink.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx deleted file mode 100644 index 22d8749d78834..0000000000000 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx +++ /dev/null @@ -1,42 +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 { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -export function Title() { - return ( - - - - - -

- {i18n.translate('xpack.apm.settings.customizeUI.customLink', { - defaultMessage: 'Custom Links', - })} -

-
- - - - -
-
-
-
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index fea22e890dc10..00fc7cd427e26 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -21,7 +21,7 @@ import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; -import * as saveCustomLink from './CustomLinkFlyout/saveCustomLink'; +import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; const data = [ diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 45a7fa2a118f2..18988aa05f2ae 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -4,19 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { useLicense } from '../../../../../hooks/useLicense'; import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; -import { CustomLinkFlyout } from './CustomLinkFlyout'; +import { LicensePrompt } from '../../../../shared/LicensePrompt'; +import { CreateCustomLinkButton } from './CreateCustomLinkButton'; +import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; import { CustomLinkTable } from './CustomLinkTable'; import { EmptyPrompt } from './EmptyPrompt'; -import { Title } from './Title'; -import { CreateCustomLinkButton } from './CreateCustomLinkButton'; -import { LicensePrompt } from '../../../../shared/LicensePrompt'; export function CustomLinkOverview() { const license = useLicense(); @@ -28,8 +34,12 @@ export function CustomLinkOverview() { >(); const { data: customLinks, status, refetch } = useFetcher( - (callApmApi) => callApmApi({ pathname: '/api/apm/settings/custom_links' }), - [] + async (callApmApi) => { + if (hasValidLicense) { + return callApmApi({ pathname: '/api/apm/settings/custom_links' }); + } + }, + [hasValidLicense] ); useEffect(() => { @@ -53,7 +63,7 @@ export function CustomLinkOverview() { return ( <> {isFlyoutOpen && ( - - + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle> + <EuiFlexGroup + alignItems="center" + gutterSize="s" + responsive={false} + > + <EuiFlexItem grow={false}> + <h2> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink', + { + defaultMessage: 'Custom Links', + } + )} + </h2> + </EuiFlexItem> + </EuiFlexGroup> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> {hasValidLicense && !showEmptyPrompt && ( <EuiFlexItem> diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx deleted file mode 100644 index 62952d1fb501b..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx +++ /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 { act, fireEvent, render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { expectTextsInDocument } from '../../../../utils/testHelpers'; -import { CustomLinkPopover } from './CustomLinkPopover'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - <MemoryRouter> - <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper> - </MemoryRouter> - ); -} - -describe('CustomLinkPopover', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'http://elastic.co' }, - { - id: '2', - label: 'bar', - url: 'http://elastic.co?service.name={{service.name}}', - }, - ] as CustomLink[]; - const transaction = ({ - service: { name: 'foo.bar' }, - } as unknown) as Transaction; - it('renders popover', () => { - const component = render( - <CustomLinkPopover - customLinks={customLinks} - transaction={transaction} - onCreateCustomLinkClick={jest.fn()} - onClose={jest.fn()} - />, - { wrapper: Wrapper } - ); - expectTextsInDocument(component, ['CUSTOM LINKS', 'Create', 'foo', 'bar']); - }); - - it('closes popover', () => { - const handleCloseMock = jest.fn(); - const { getByText } = render( - <CustomLinkPopover - customLinks={customLinks} - transaction={transaction} - onCreateCustomLinkClick={jest.fn()} - onClose={handleCloseMock} - />, - { wrapper: Wrapper } - ); - expect(handleCloseMock).not.toHaveBeenCalled(); - act(() => { - fireEvent.click(getByText('CUSTOM LINKS')); - }); - expect(handleCloseMock).toHaveBeenCalled(); - }); - - it('opens flyout to create new custom link', () => { - const handleCreateCustomLinkClickMock = jest.fn(); - const { getByText } = render( - <CustomLinkPopover - customLinks={customLinks} - transaction={transaction} - onCreateCustomLinkClick={handleCreateCustomLinkClickMock} - onClose={jest.fn()} - />, - { wrapper: Wrapper } - ); - expect(handleCreateCustomLinkClickMock).not.toHaveBeenCalled(); - act(() => { - fireEvent.click(getByText('Create')); - }); - expect(handleCreateCustomLinkClickMock).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx deleted file mode 100644 index 27c6aa82ac674..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx +++ /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 React from 'react'; -import { - EuiPopoverTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { CustomLinkSection } from './CustomLinkSection'; -import { ManageCustomLink } from './ManageCustomLink'; -import { px } from '../../../../style/variables'; - -const ScrollableContainer = styled.div` - -ms-overflow-style: none; - max-height: ${px(535)}; - overflow: scroll; -`; - -export function CustomLinkPopover({ - customLinks, - onCreateCustomLinkClick, - onClose, - transaction, -}: { - customLinks: CustomLink[]; - onCreateCustomLinkClick: () => void; - onClose: () => void; - transaction: Transaction; -}) { - return ( - <> - <EuiPopoverTitle> - <EuiFlexGroup> - <EuiFlexItem style={{ alignItems: 'flex-start' }}> - <EuiButtonEmpty - color="text" - size="xs" - onClick={onClose} - iconType="arrowLeft" - style={{ fontWeight: 'bold' }} - flush="left" - > - {i18n.translate( - 'xpack.apm.transactionActionMenu.customLink.popover.title', - { - defaultMessage: 'CUSTOM LINKS', - } - )} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem> - <ManageCustomLink - onCreateCustomLinkClick={onCreateCustomLinkClick} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPopoverTitle> - <ScrollableContainer> - <CustomLinkSection - customLinks={customLinks} - transaction={transaction} - /> - </ScrollableContainer> - </> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx deleted file mode 100644 index 6b421bc370332..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx +++ /dev/null @@ -1,53 +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 { EuiLink, EuiText } from '@elastic/eui'; -import Mustache from 'mustache'; -import React from 'react'; -import styled from 'styled-components'; -import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { px, truncate, units } from '../../../../style/variables'; - -const LinkContainer = styled.li` - margin-top: ${px(units.half)}; - &:first-of-type { - margin-top: 0; - } -`; - -const TruncateText = styled(EuiText)` - font-weight: 500; - line-height: ${px(units.unit)}; - ${truncate(px(units.unit * 25))} -`; - -export function CustomLinkSection({ - customLinks, - transaction, -}: { - customLinks: CustomLink[]; - transaction: Transaction; -}) { - return ( - <ul> - {customLinks.map((link) => { - let href = link.url; - try { - href = Mustache.render(link.url, transaction); - } catch (e) { - // ignores any error that happens - } - return ( - <LinkContainer key={link.id}> - <EuiLink href={href} target="_blank"> - <TruncateText size="s">{link.label}</TruncateText> - </EuiLink> - </LinkContainer> - ); - })} - </ul> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx deleted file mode 100644 index d6484f52e84f9..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx +++ /dev/null @@ -1,128 +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 React from 'react'; -import { - EuiText, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiButtonEmpty, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; -import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { - ActionMenuDivider, - SectionSubtitle, -} from '../../../../../../observability/public'; -import { CustomLinkSection } from './CustomLinkSection'; -import { ManageCustomLink } from './ManageCustomLink'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { LoadingStatePrompt } from '../../LoadingStatePrompt'; -import { px } from '../../../../style/variables'; - -const SeeMoreButton = styled.button<{ show: boolean }>` - display: ${(props) => (props.show ? 'flex' : 'none')}; - align-items: center; - width: 100%; - justify-content: space-between; - &:hover { - text-decoration: underline; - } -`; - -export function CustomLink({ - customLinks, - status, - onCreateCustomLinkClick, - onSeeMoreClick, - transaction, -}: { - customLinks: CustomLinkType[]; - status: FETCH_STATUS; - onCreateCustomLinkClick: () => void; - onSeeMoreClick: () => void; - transaction: Transaction; -}) { - const renderEmptyPrompt = ( - <> - <EuiText size="xs" grow={false} style={{ width: px(300) }}> - {i18n.translate('xpack.apm.customLink.empty', { - defaultMessage: - 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.', - })} - </EuiText> - <EuiSpacer size="s" /> - <EuiButtonEmpty - iconType="plusInCircle" - size="xs" - onClick={onCreateCustomLinkClick} - > - {i18n.translate('xpack.apm.customLink.buttom.create', { - defaultMessage: 'Create custom link', - })} - </EuiButtonEmpty> - </> - ); - - const renderCustomLinkBottomSection = isEmpty(customLinks) ? ( - renderEmptyPrompt - ) : ( - <SeeMoreButton onClick={onSeeMoreClick} show={customLinks.length > 3}> - <EuiText size="s"> - {i18n.translate('xpack.apm.transactionActionMenu.customLink.seeMore', { - defaultMessage: 'See more', - })} - </EuiText> - <EuiIcon type="arrowRight" /> - </SeeMoreButton> - ); - - return ( - <> - <ActionMenuDivider /> - <EuiFlexGroup> - <EuiFlexItem style={{ justifyContent: 'center' }}> - <EuiText size={'s'} grow={false}> - <h5> - {i18n.translate( - 'xpack.apm.transactionActionMenu.customLink.section', - { - defaultMessage: 'Custom Links', - } - )} - </h5> - </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <ManageCustomLink - onCreateCustomLinkClick={onCreateCustomLinkClick} - showCreateCustomLinkButton={!!customLinks.length} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="s" /> - <SectionSubtitle> - {i18n.translate('xpack.apm.transactionActionMenu.customLink.subtitle', { - defaultMessage: 'Links will open in a new window.', - })} - </SectionSubtitle> - <CustomLinkSection - customLinks={customLinks.slice(0, 3)} - transaction={transaction} - /> - <EuiSpacer size="s" /> - {status === FETCH_STATUS.LOADING ? ( - <LoadingStatePrompt /> - ) : ( - renderCustomLinkBottomSection - )} - </> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx similarity index 82% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx index 88a4137b47200..16d526bda2103 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import { CustomLinkSection } from './CustomLinkSection'; +import { CustomLinkList } from './CustomLinkList'; import { expectTextsInDocument, expectTextsNotInDocument, @@ -13,7 +13,7 @@ import { import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; -describe('CustomLinkSection', () => { +describe('CustomLinkList', () => { const customLinks = [ { id: '1', label: 'foo', url: 'http://elastic.co' }, { @@ -27,14 +27,14 @@ describe('CustomLinkSection', () => { } as unknown) as Transaction; it('shows links', () => { const component = render( - <CustomLinkSection customLinks={customLinks} transaction={transaction} /> + <CustomLinkList customLinks={customLinks} transaction={transaction} /> ); expectTextsInDocument(component, ['foo', 'bar']); }); it('doesnt show any links', () => { const component = render( - <CustomLinkSection customLinks={[]} transaction={transaction} /> + <CustomLinkList customLinks={[]} transaction={transaction} /> ); expectTextsNotInDocument(component, ['foo', 'bar']); }); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx new file mode 100644 index 0000000000000..0304b850d6cee --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Mustache from 'mustache'; +import React from 'react'; +import { + SectionLinks, + SectionLink, +} from '../../../../../../observability/public'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { px, unit } from '../../../../style/variables'; + +export function CustomLinkList({ + customLinks, + transaction, +}: { + customLinks: CustomLink[]; + transaction: Transaction; +}) { + return ( + <SectionLinks style={{ maxHeight: px(unit * 10), overflowY: 'auto' }}> + {customLinks.map((link) => { + const href = getHref(link, transaction); + return ( + <SectionLink + key={link.id} + label={link.label} + href={href} + target="_blank" + /> + ); + })} + </SectionLinks> + ); +} + +function getHref(link: CustomLink, transaction: Transaction) { + try { + return Mustache.render(link.url, transaction); + } catch (e) { + return link.url; + } +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx similarity index 78% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx index 29e93a47629b3..0241167aba1fb 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx @@ -12,7 +12,7 @@ import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../utils/testHelpers'; -import { ManageCustomLink } from './ManageCustomLink'; +import { CustomLinkToolbar } from './CustomLinkToolbar'; function Wrapper({ children }: { children?: ReactNode }) { return ( @@ -22,23 +22,20 @@ function Wrapper({ children }: { children?: ReactNode }) { ); } -describe('ManageCustomLink', () => { +describe('CustomLinkToolbar', () => { it('renders with create button', () => { - const component = render( - <ManageCustomLink onCreateCustomLinkClick={jest.fn()} />, - { wrapper: Wrapper } - ); + const component = render(<CustomLinkToolbar onClickCreate={jest.fn()} />, { + wrapper: Wrapper, + }); expect( component.getByLabelText('Custom links settings page') ).toBeInTheDocument(); expectTextsInDocument(component, ['Create']); }); + it('renders without create button', () => { const component = render( - <ManageCustomLink - onCreateCustomLinkClick={jest.fn()} - showCreateCustomLinkButton={false} - />, + <CustomLinkToolbar onClickCreate={jest.fn()} showCreateButton={false} />, { wrapper: Wrapper } ); expect( @@ -46,12 +43,11 @@ describe('ManageCustomLink', () => { ).toBeInTheDocument(); expectTextsNotInDocument(component, ['Create']); }); + it('opens flyout to create new custom link', () => { const handleCreateCustomLinkClickMock = jest.fn(); const { getByText } = render( - <ManageCustomLink - onCreateCustomLinkClick={handleCreateCustomLinkClickMock} - />, + <CustomLinkToolbar onClickCreate={handleCreateCustomLinkClickMock} />, { wrapper: Wrapper } ); expect(handleCreateCustomLinkClickMock).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx similarity index 85% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx index 09cdaa26004bb..36b370b4069ae 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx @@ -14,12 +14,12 @@ import { import { i18n } from '@kbn/i18n'; import { APMLink } from '../../Links/apm/APMLink'; -export function ManageCustomLink({ - onCreateCustomLinkClick, - showCreateCustomLinkButton = true, +export function CustomLinkToolbar({ + onClickCreate, + showCreateButton = true, }: { - onCreateCustomLinkClick: () => void; - showCreateCustomLinkButton?: boolean; + onClickCreate: () => void; + showCreateButton?: boolean; }) { return ( <EuiFlexGroup> @@ -41,12 +41,12 @@ export function ManageCustomLink({ </APMLink> </EuiToolTip> </EuiFlexItem> - {showCreateCustomLinkButton && ( + {showCreateButton && ( <EuiFlexItem grow={false}> <EuiButtonEmpty iconType="plusInCircle" size="xs" - onClick={onCreateCustomLinkClick} + onClick={onClickCreate} > {i18n.translate('xpack.apm.customLink.buttom.create.title', { defaultMessage: 'Create', diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx index 5abeae265dfa6..db7a284f6adff 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx @@ -7,11 +7,11 @@ import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { CustomLink } from '.'; +import { CustomLinkMenuSection } from '.'; import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import * as useFetcher from '../../../../hooks/useFetcher'; import { expectTextsInDocument, expectTextsNotInDocument, @@ -25,16 +25,27 @@ function Wrapper({ children }: { children?: ReactNode }) { ); } +const transaction = ({ + service: { + name: 'name', + environment: 'env', + }, + transaction: { + name: 'tx name', + type: 'tx type', + }, +} as unknown) as Transaction; + describe('Custom links', () => { it('shows empty message when no custom link is available', () => { + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: [], + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); @@ -45,14 +56,14 @@ describe('Custom links', () => { }); it('shows loading while custom links are fetched', () => { + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: [], + status: useFetcher.FETCH_STATUS.LOADING, + refetch: jest.fn(), + }); + const { getByTestId } = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.LOADING} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expect(getByTestId('loading-spinner')).toBeInTheDocument(); @@ -65,61 +76,68 @@ describe('Custom links', () => { { id: '3', label: 'baz', url: 'baz' }, { id: '4', label: 'qux', url: 'qux' }, ] as CustomLinkType[]; + + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: customLinks, + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expectTextsInDocument(component, ['foo', 'bar', 'baz']); expectTextsNotInDocument(component, ['qux']); }); - it('clicks on See more button', () => { + it('clicks "show all" and "show fewer"', () => { const customLinks = [ { id: '1', label: 'foo', url: 'foo' }, { id: '2', label: 'bar', url: 'bar' }, { id: '3', label: 'baz', url: 'baz' }, { id: '4', label: 'qux', url: 'qux' }, ] as CustomLinkType[]; - const onSeeMoreClickMock = jest.fn(); + + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: customLinks, + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={onSeeMoreClickMock} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); - expect(onSeeMoreClickMock).not.toHaveBeenCalled(); + + expect(component.getAllByRole('listitem').length).toEqual(3); + act(() => { + fireEvent.click(component.getByText('Show all')); + }); + expect(component.getAllByRole('listitem').length).toEqual(4); act(() => { - fireEvent.click(component.getByText('See more')); + fireEvent.click(component.getByText('Show fewer')); }); - expect(onSeeMoreClickMock).toHaveBeenCalled(); + expect(component.getAllByRole('listitem').length).toEqual(3); }); describe('create custom link buttons', () => { it('shows create button below empty message', () => { + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: [], + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expectTextsInDocument(component, ['Create custom link']); expectTextsNotInDocument(component, ['Create']); }); + it('shows create button besides the title', () => { const customLinks = [ { id: '1', label: 'foo', url: 'foo' }, @@ -127,14 +145,15 @@ describe('Custom links', () => { { id: '3', label: 'baz', url: 'baz' }, { id: '4', label: 'qux', url: 'qux' }, ] as CustomLinkType[]; + + jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ + data: customLinks, + status: useFetcher.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - />, + <CustomLinkMenuSection transaction={transaction} />, { wrapper: Wrapper } ); expectTextsInDocument(component, ['Create']); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx new file mode 100644 index 0000000000000..ec57c726f7246 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx @@ -0,0 +1,209 @@ +/* + * 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, { useMemo, useState } from 'react'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { + ActionMenuDivider, + Section, + SectionSubtitle, + SectionTitle, +} from '../../../../../../observability/public'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { CustomLinkList } from './CustomLinkList'; +import { CustomLinkToolbar } from './CustomLinkToolbar'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { LoadingStatePrompt } from '../../LoadingStatePrompt'; +import { px } from '../../../../style/variables'; +import { CreateEditCustomLinkFlyout } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout'; +import { convertFiltersToQuery } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper'; +import { + CustomLink, + Filter, +} from '../../../../../common/custom_link/custom_link_types'; + +const DEFAULT_LINKS_TO_SHOW = 3; + +export function CustomLinkMenuSection({ + transaction, +}: { + transaction: Transaction; +}) { + const [showAllLinks, setShowAllLinks] = useState(false); + const [isCreateEditFlyoutOpen, setIsCreateEditFlyoutOpen] = useState(false); + + const filters = useMemo(() => { + if (!transaction) { + return []; + } + + return [ + { key: 'service.name', value: transaction.service.name }, + { key: 'service.environment', value: transaction.service.environment }, + { key: 'transaction.name', value: transaction.transaction.name }, + { key: 'transaction.type', value: transaction.transaction.type }, + ].filter((filter): filter is Filter => typeof filter.value === 'string'); + }, [transaction]); + + const { data: customLinks = [], status, refetch } = useFetcher( + (callApmApi) => + callApmApi({ + isCachable: true, + pathname: '/api/apm/settings/custom_links', + params: { query: convertFiltersToQuery(filters) }, + }), + [filters] + ); + + return ( + <> + {isCreateEditFlyoutOpen && ( + <CreateEditCustomLinkFlyout + defaults={{ filters }} + onClose={() => { + setIsCreateEditFlyoutOpen(false); + }} + onSave={() => { + setIsCreateEditFlyoutOpen(false); + refetch(); + }} + onDelete={() => { + setIsCreateEditFlyoutOpen(false); + refetch(); + }} + /> + )} + + <ActionMenuDivider /> + + <Section> + <EuiFlexGroup> + <EuiFlexItem> + <SectionTitle> + {i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.section', + { + defaultMessage: 'Custom Links', + } + )} + </SectionTitle> + </EuiFlexItem> + <EuiFlexItem> + <CustomLinkToolbar + onClickCreate={() => setIsCreateEditFlyoutOpen(true)} + showCreateButton={customLinks.length > 0} + /> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + <SectionSubtitle> + {i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.subtitle', + { + defaultMessage: 'Links will open in a new window.', + } + )} + </SectionSubtitle> + <CustomLinkList + customLinks={ + showAllLinks + ? customLinks + : customLinks.slice(0, DEFAULT_LINKS_TO_SHOW) + } + transaction={transaction} + /> + <EuiSpacer size="s" /> + <BottomSection + status={status} + customLinks={customLinks} + showAllLinks={showAllLinks} + toggleShowAll={() => setShowAllLinks((show) => !show)} + onClickCreate={() => setIsCreateEditFlyoutOpen(true)} + /> + </Section> + </> + ); +} + +function BottomSection({ + status, + customLinks, + showAllLinks, + toggleShowAll, + onClickCreate, +}: { + status: FETCH_STATUS; + customLinks: CustomLink[]; + showAllLinks: boolean; + toggleShowAll: () => void; + onClickCreate: () => void; +}) { + if (status === FETCH_STATUS.LOADING) { + return <LoadingStatePrompt />; + } + + // render empty prompt if there are no custom links + if (isEmpty(customLinks)) { + return ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiText size="xs" grow={false} style={{ width: px(300) }}> + {i18n.translate('xpack.apm.customLink.empty', { + defaultMessage: + 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.', + })} + </EuiText> + <EuiSpacer size="s" /> + <EuiButtonEmpty + iconType="plusInCircle" + size="xs" + onClick={onClickCreate} + > + {i18n.translate('xpack.apm.customLink.buttom.create', { + defaultMessage: 'Create custom link', + })} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + ); + } + + // render button to toggle "Show all" / "Show fewer" + if (customLinks.length > DEFAULT_LINKS_TO_SHOW) { + return ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiButtonEmpty + iconType={showAllLinks ? 'arrowUp' : 'arrowDown'} + onClick={toggleShowAll} + > + <EuiText size="s"> + {showAllLinks + ? i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.showFewer', + { defaultMessage: 'Show fewer' } + ) + : i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.showAll', + { defaultMessage: 'Show all' } + )} + </EuiText> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + ); + } + + return null; +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 4a548b44cf361..15a85113406e1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -6,7 +6,7 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { ActionMenu, @@ -17,16 +17,11 @@ import { SectionSubtitle, SectionTitle, } from '../../../../../observability/public'; -import { Filter } from '../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { CustomLinkFlyout } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout'; -import { convertFiltersToQuery } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper'; -import { CustomLink } from './CustomLink'; -import { CustomLinkPopover } from './CustomLink/CustomLinkPopover'; +import { CustomLinkMenuSection } from './CustomLinkMenuSection'; import { getSections } from './sections'; interface Props { @@ -45,38 +40,13 @@ function ActionMenuButton({ onClick }: { onClick: () => void }) { export function TransactionActionMenu({ transaction }: Props) { const license = useLicense(); - const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); + const hasGoldLicense = license?.isActive && license?.hasAtLeast('gold'); const { core } = useApmPluginContext(); const location = useLocation(); const { urlParams } = useUrlParams(); const [isActionPopoverOpen, setIsActionPopoverOpen] = useState(false); - const [isCustomLinksPopoverOpen, setIsCustomLinksPopoverOpen] = useState( - false - ); - const [isCustomLinkFlyoutOpen, setIsCustomLinkFlyoutOpen] = useState(false); - - const filters = useMemo( - () => - [ - { key: 'service.name', value: transaction?.service.name }, - { key: 'service.environment', value: transaction?.service.environment }, - { key: 'transaction.name', value: transaction?.transaction.name }, - { key: 'transaction.type', value: transaction?.transaction.type }, - ].filter((filter): filter is Filter => typeof filter.value === 'string'), - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - [transaction] - ); - - const { data: customLinks = [], status, refetch } = useFetcher( - (callApmApi) => - callApmApi({ - pathname: '/api/apm/settings/custom_links', - params: { query: convertFiltersToQuery(filters) }, - }), - [filters] - ); const sections = getSections({ transaction, @@ -85,39 +55,11 @@ export function TransactionActionMenu({ transaction }: Props) { urlParams, }); - const closePopover = () => { - setIsActionPopoverOpen(false); - setIsCustomLinksPopoverOpen(false); - }; - - const toggleCustomLinkFlyout = () => { - closePopover(); - setIsCustomLinkFlyoutOpen((isOpen) => !isOpen); - }; - - const toggleCustomLinkPopover = () => { - setIsCustomLinksPopoverOpen((isOpen) => !isOpen); - }; - return ( <> - {isCustomLinkFlyoutOpen && ( - <CustomLinkFlyout - defaults={{ filters }} - onClose={toggleCustomLinkFlyout} - onSave={() => { - toggleCustomLinkFlyout(); - refetch(); - }} - onDelete={() => { - toggleCustomLinkFlyout(); - refetch(); - }} - /> - )} <ActionMenu id="transactionActionMenu" - closePopover={closePopover} + closePopover={() => setIsActionPopoverOpen(false)} isOpen={isActionPopoverOpen} anchorPosition="downRight" button={ @@ -125,52 +67,34 @@ export function TransactionActionMenu({ transaction }: Props) { } > <div> - {isCustomLinksPopoverOpen ? ( - <CustomLinkPopover - customLinks={customLinks.slice(3, customLinks.length)} - onCreateCustomLinkClick={toggleCustomLinkFlyout} - onClose={toggleCustomLinkPopover} - transaction={transaction} - /> - ) : ( - <> - {sections.map((section, idx) => { - const isLastSection = idx !== sections.length - 1; - return ( - <div key={idx}> - {section.map((item) => ( - <Section key={item.key}> - {item.title && ( - <SectionTitle>{item.title}</SectionTitle> - )} - {item.subtitle && ( - <SectionSubtitle>{item.subtitle}</SectionSubtitle> - )} - <SectionLinks> - {item.actions.map((action) => ( - <SectionLink - key={action.key} - label={action.label} - href={action.href} - /> - ))} - </SectionLinks> - </Section> - ))} - {isLastSection && <ActionMenuDivider />} - </div> - ); - })} - {hasValidLicense && ( - <CustomLink - customLinks={customLinks} - status={status} - onCreateCustomLinkClick={toggleCustomLinkFlyout} - onSeeMoreClick={toggleCustomLinkPopover} - transaction={transaction} - /> - )} - </> + {sections.map((section, idx) => { + const isLastSection = idx !== sections.length - 1; + return ( + <div key={idx}> + {section.map((item) => ( + <Section key={item.key}> + {item.title && <SectionTitle>{item.title}</SectionTitle>} + {item.subtitle && ( + <SectionSubtitle>{item.subtitle}</SectionSubtitle> + )} + <SectionLinks> + {item.actions.map((action) => ( + <SectionLink + key={action.key} + label={action.label} + href={action.href} + /> + ))} + </SectionLinks> + </Section> + ))} + {isLastSection && <ActionMenuDivider />} + </div> + ); + })} + + {hasGoldLicense && ( + <CustomLinkMenuSection transaction={transaction} /> )} </div> </ActionMenu> diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index 91fc05137a8c1..22db150f1de5e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -101,7 +101,7 @@ export async function fetchMissingMonitoringData( 'kibana_stats.kibana.name', 'logstash_stats.logstash.host', 'beats_stats.beat.name', - 'beat_stats.beat.type', + 'beats_stats.beat.type', ]; const subAggs = { most_recent: { diff --git a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx index 55746ff6576a9..4819a0760d88a 100644 --- a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx @@ -12,11 +12,11 @@ import { EuiHorizontalRule, EuiListGroupItem, EuiPopoverProps, + EuiListGroupItemProps, } from '@elastic/eui'; - import React, { HTMLAttributes, ReactNode } from 'react'; -import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; import styled from 'styled-components'; +import { EuiListGroupProps } from '@elastic/eui'; type Props = EuiPopoverProps & HTMLAttributes<HTMLDivElement>; @@ -42,9 +42,9 @@ export function SectionSubtitle({ children }: { children?: ReactNode }) { ); } -export function SectionLinks({ children }: { children?: ReactNode }) { +export function SectionLinks({ children, ...props }: { children?: ReactNode } & EuiListGroupProps) { return ( - <EuiListGroup flush={true} bordered={false}> + <EuiListGroup {...props} flush={true} bordered={false}> {children} </EuiListGroup> ); diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 24620f641c204..8f988825ec987 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -30,8 +30,8 @@ const EuiCardWithoutPadding = styled(EuiCard)` `; export function LandingPage() { - useTrackPageview({ app: 'observability', path: 'landing' }); - useTrackPageview({ app: 'observability', path: 'landing', delay: 15000 }); + useTrackPageview({ app: 'observability-overview', path: 'landing' }); + useTrackPageview({ app: 'observability-overview', path: 'landing', delay: 15000 }); const { core } = usePluginContext(); const theme = useContext(ThemeContext); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index ff91b08d709aa..1fa9bef88a930 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -55,8 +55,8 @@ export function OverviewPage({ routeParams }: Props) { end: getAbsoluteTime(relativeTime.end, { roundUp: true }) as number, }; - useTrackPageview({ app: 'observability', path: 'overview' }); - useTrackPageview({ app: 'observability', path: 'overview', delay: 15000 }); + useTrackPageview({ app: 'observability-overview', path: 'overview' }); + useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 }); const { data: alerts = [], status: alertStatus } = useFetcher(() => { return getObservabilityAlerts({ core }); diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index a64e6fc55b85a..70c1eb1859ee3 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -47,7 +47,7 @@ export type HasData = (params?: HasDataParams) => Promise<HasDataResponse>; export type ObservabilityFetchDataPlugins = Exclude< ObservabilityApp, - 'observability' | 'stack_monitoring' + 'observability-overview' | 'stack_monitoring' >; export interface DataHandler< diff --git a/x-pack/plugins/observability/typings/common.ts b/x-pack/plugins/observability/typings/common.ts index c86eb924a051e..8093d6077148e 100644 --- a/x-pack/plugins/observability/typings/common.ts +++ b/x-pack/plugins/observability/typings/common.ts @@ -9,7 +9,7 @@ export type ObservabilityApp = | 'infra_logs' | 'apm' | 'uptime' - | 'observability' + | 'observability-overview' | 'stack_monitoring' | 'ux'; diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx index e65310ba399ea..5479bc36d1ed5 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx @@ -83,18 +83,18 @@ describe('roleMappingsManagementApp', () => { }); it('mount() works for the `edit role mapping` page', async () => { - const roleMappingName = 'someRoleMappingName'; + const roleMappingName = 'role@mapping'; const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${roleMappingName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Role Mappings' }, - { href: `/edit/${roleMappingName}`, text: roleMappingName }, + { href: `/edit/${encodeURIComponent(roleMappingName)}`, text: roleMappingName }, ]); expect(container).toMatchInlineSnapshot(` <div> - Role Mapping Edit Page: {"name":"someRoleMappingName","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/someRoleMappingName","search":"","hash":""}}} + Role Mapping Edit Page: {"name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}} </div> `); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index bca3a070e64f9..ce4ded5a9acbc 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -12,6 +12,7 @@ import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { PluginStartDependencies } from '../../plugin'; import { DocumentationLinksService } from './documentation_links'; +import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { getStartServices: StartServicesAccessor<PluginStartDependencies>; @@ -70,10 +71,14 @@ export const roleMappingsManagementApp = Object.freeze({ const EditRoleMappingsPageWithBreadcrumbs = () => { const { name } = useParams<{ name?: string }>(); + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const decodedName = name ? tryDecodeURIComponent(name) : undefined; + setBreadcrumbs([ ...roleMappingsBreadcrumbs, name - ? { text: name, href: `/edit/${encodeURIComponent(name)}` } + ? { text: decodedName, href: `/edit/${encodeURIComponent(name)}` } : { text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { defaultMessage: 'Create', @@ -83,7 +88,7 @@ export const roleMappingsManagementApp = Object.freeze({ return ( <EditRoleMappingPage - name={name} + name={decodedName} roleMappingsAPI={roleMappingsAPIClient} rolesAPIClient={new RolesAPIClient(http)} notifications={notifications} diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx index c45528399db99..8bcf58428c08d 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -97,18 +97,18 @@ describe('rolesManagementApp', () => { }); it('mount() works for the `edit role` page', async () => { - const roleName = 'someRoleName'; + const roleName = 'role@name'; const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${roleName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Roles' }, - { href: `/edit/${roleName}`, text: roleName }, + { href: `/edit/${encodeURIComponent(roleName)}`, text: roleName }, ]); expect(container).toMatchInlineSnapshot(` <div> - Role Edit Page: {"action":"edit","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/someRoleName","search":"","hash":""}}} + Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}} </div> `); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index 88aeb1d232fc7..d5b3b4998a09d 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -13,6 +13,7 @@ import { RegisterManagementAppArgs } from '../../../../../../src/plugins/managem import { SecurityLicense } from '../../../common/licensing'; import { PluginStartDependencies } from '../../plugin'; import { DocumentationLinksService } from './documentation_links'; +import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { fatalErrors: FatalErrorsSetup; @@ -68,10 +69,14 @@ export const rolesManagementApp = Object.freeze({ const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { const { roleName } = useParams<{ roleName?: string }>(); + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const decodedRoleName = roleName ? tryDecodeURIComponent(roleName) : undefined; + setBreadcrumbs([ ...rolesBreadcrumbs, action === 'edit' && roleName - ? { text: roleName, href: `/edit/${encodeURIComponent(roleName)}` } + ? { text: decodedRoleName, href: `/edit/${encodeURIComponent(roleName)}` } : { text: i18n.translate('xpack.security.roles.createBreadcrumb', { defaultMessage: 'Create', @@ -82,7 +87,7 @@ export const rolesManagementApp = Object.freeze({ return ( <EditRolePage action={action} - roleName={roleName} + roleName={decodedRoleName} rolesAPIClient={rolesAPIClient} userAPIClient={new UserAPIClient(http)} indicesAPIClient={new IndicesAPIClient(http)} diff --git a/x-pack/plugins/security/public/management/uri_utils.test.ts b/x-pack/plugins/security/public/management/uri_utils.test.ts new file mode 100644 index 0000000000000..029228d911c05 --- /dev/null +++ b/x-pack/plugins/security/public/management/uri_utils.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { tryDecodeURIComponent } from './url_utils'; + +describe('tryDecodeURIComponent', () => { + it('properly decodes a URI Component', () => { + expect( + tryDecodeURIComponent('sample%26piece%3Dof%20text%40gmail.com%2520') + ).toMatchInlineSnapshot(`"sample&piece=of text@gmail.com%20"`); + }); + + it('returns the original string undecoded if it is malformed', () => { + expect(tryDecodeURIComponent('sample&piece=of%text@gmail.com%20')).toMatchInlineSnapshot( + `"sample&piece=of%text@gmail.com%20"` + ); + }); +}); diff --git a/x-pack/plugins/security/public/management/url_utils.ts b/x-pack/plugins/security/public/management/url_utils.ts new file mode 100644 index 0000000000000..590863e30d5ec --- /dev/null +++ b/x-pack/plugins/security/public/management/url_utils.ts @@ -0,0 +1,13 @@ +/* + * 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 tryDecodeURIComponent = (uriComponent: string) => { + try { + return decodeURIComponent(uriComponent); + } catch { + return uriComponent; + } +}; diff --git a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx index 06bd2eff6aa1e..c9e448d90d925 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx @@ -86,18 +86,18 @@ describe('usersManagementApp', () => { }); it('mount() works for the `edit user` page', async () => { - const userName = 'someUserName'; + const userName = 'foo@bar.com'; const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${userName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Users' }, - { href: `/edit/${userName}`, text: userName }, + { href: `/edit/${encodeURIComponent(userName)}`, text: userName }, ]); expect(container).toMatchInlineSnapshot(` <div> - User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/someUserName","search":"","hash":""}}} + User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"foo@bar.com","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/foo@bar.com","search":"","hash":""}}} </div> `); @@ -106,18 +106,23 @@ describe('usersManagementApp', () => { expect(container).toMatchInlineSnapshot(`<div />`); }); - it('mount() properly encodes user name in `edit user` page link in breadcrumbs', async () => { - const username = 'some 安全性 user'; - - const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { - href: '/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', - text: username, - }, - ]); + const usernames = ['foo@bar.com', 'foo&bar.com', 'some 安全性 user']; + usernames.forEach((username) => { + it( + 'mount() properly encodes user name in `edit user` page link in breadcrumbs for user ' + + username, + async () => { + const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `/`, text: 'Users' }, + { + href: `/edit/${encodeURIComponent(username)}`, + text: username, + }, + ]); + } + ); }); }); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 82c55d67b9026..2f16f85d5fcae 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -12,6 +12,7 @@ import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; +import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { authc: AuthenticationServiceSetup; @@ -66,10 +67,14 @@ export const usersManagementApp = Object.freeze({ const EditUserPageWithBreadcrumbs = () => { const { username } = useParams<{ username?: string }>(); + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const decodedUsername = username ? tryDecodeURIComponent(username) : undefined; + setBreadcrumbs([ ...usersBreadcrumbs, username - ? { text: username, href: `/edit/${encodeURIComponent(username)}` } + ? { text: decodedUsername, href: `/edit/${encodeURIComponent(username)}` } : { text: i18n.translate('xpack.security.users.createBreadcrumb', { defaultMessage: 'Create', @@ -83,7 +88,7 @@ export const usersManagementApp = Object.freeze({ userAPIClient={userAPIClient} rolesAPIClient={new RolesAPIClient(http)} notifications={notifications} - username={username} + username={decodedUsername} history={history} /> ); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2910f02a187f4..4d7dfb13ad699 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -159,6 +159,9 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.slack', '.pagerduty', '.webhook', + '.servicenow', + '.jira', + '.resilient', ]; export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; export const NOTIFICATION_THROTTLE_RULE = 'rule'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index cc454ac1e9462..07abac22c55d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -103,6 +103,14 @@ export const buildSignalGroupFromSequence = ( outputIndex ); + if ( + wrappedBuildingBlocks.some((block) => + block._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleSO.id) + ) + ) { + return []; + } + // Now that we have an array of building blocks for the events in the sequence, // we can build the signal that links the building blocks together // and also insert the group id (which is also the "shell" signal _id) in each building block diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index bb3a0b4fa6f08..1d73a0f9ab174 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -58,7 +58,7 @@ import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects import { getNotificationResultsLink } from '../notifications/utils'; import { TelemetryEventsSender } from '../../telemetry/sender'; import { buildEqlSearchRequest } from '../../../../common/detection_engine/get_query_filter'; -import { bulkInsertSignals } from './single_bulk_create'; +import { bulkInsertSignals, filterDuplicateSignals } from './single_bulk_create'; import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk_body'; import { createThreatSignals } from './threat_mapping/create_threat_signals'; import { getIndexVersion } from '../routes/index/get_index_version'; @@ -489,16 +489,17 @@ export const signalRulesAlertType = ({ [] ); } else if (response.hits.events !== undefined) { - newSignals = response.hits.events.map((event) => - wrapSignal(buildSignalFromEvent(event, savedObject, true), outputIndex) + newSignals = filterDuplicateSignals( + savedObject.id, + response.hits.events.map((event) => + wrapSignal(buildSignalFromEvent(event, savedObject, true), outputIndex) + ) ); } else { throw new Error( 'eql query response should have either `sequences` or `events` but had neither' ); } - // TODO: replace with code that filters out recursive rule signals while allowing sequences and their building blocks - // const filteredSignals = filterDuplicateSignals(alertId, newSignals); if (newSignals.length > 0) { const insertResult = await bulkInsertSignals(newSignals, logger, services, refresh); result.bulkCreateTimes.push(insertResult.bulkCreateDuration); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index 759890cc9d074..77c415d825c18 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -7,7 +7,7 @@ import { countBy, isEmpty } from 'lodash'; import { performance } from 'perf_hooks'; import { AlertServices } from '../../../../../alerts/server'; -import { SignalSearchResponse, BulkResponse, SignalHit, BaseSignalHit } from './types'; +import { SignalSearchResponse, BulkResponse, BaseSignalHit } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { generateId, makeFloatString, errorAggregator } from './utils'; @@ -67,9 +67,9 @@ export const filterDuplicateRules = ( * @param ruleId The rule id * @param signals The candidate new signals */ -export const filterDuplicateSignals = (ruleId: string, signals: SignalHit[]) => { +export const filterDuplicateSignals = (ruleId: string, signals: BaseSignalHit[]) => { return signals.filter( - (doc) => !doc.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) + (doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId) ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ef6d83e3a1bea..13499d09db870 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5198,7 +5198,6 @@ "xpack.apm.settings.customizeUI.customLink.flyout.required": "必須", "xpack.apm.settings.customizeUI.customLink.flyout.save": "保存", "xpack.apm.settings.customizeUI.customLink.flyout.title": "リンクを作成", - "xpack.apm.settings.customizeUI.customLink.info": "これらのリンクは、トランザクションの「アクション」コンテキストメニューに表示されます。", "xpack.apm.settings.customizeUI.customLink.license.text": "カスタムリンクを作成するには、Elastic Gold 以上のライセンスが必要です。適切なライセンスがあれば、カスタムリンクを作成してサービスを分析する際にワークフローを改良できます。", "xpack.apm.settings.customizeUI.customLink.linkPreview.descrition": "上記のフィルターに基づき、サンプルトランザクションドキュメントの値でリンクをテストしてください。", "xpack.apm.settings.customizeUI.customLink.preview.contextVariable.invalid": "無効な変数が定義されているため、サンプルトランザクションドキュメントが見つかりませんでした。", @@ -5233,9 +5232,7 @@ "xpack.apm.transactionActionMenu.actionsButtonLabel": "アクション", "xpack.apm.transactionActionMenu.container.subtitle": "このコンテナーのログとインデックスを表示し、さらに詳細を確認できます。", "xpack.apm.transactionActionMenu.container.title": "コンテナーの詳細", - "xpack.apm.transactionActionMenu.customLink.popover.title": "カスタムリンク", "xpack.apm.transactionActionMenu.customLink.section": "カスタムリンク", - "xpack.apm.transactionActionMenu.customLink.seeMore": "詳細を表示", "xpack.apm.transactionActionMenu.customLink.subtitle": "リンクは新しいウィンドウで開きます。", "xpack.apm.transactionActionMenu.host.subtitle": "ホストログとメトリックを表示し、さらに詳細を確認できます。", "xpack.apm.transactionActionMenu.host.title": "ホストの詳細", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index df9e9f536dfa2..3e9ec7446b18e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5202,7 +5202,6 @@ "xpack.apm.settings.customizeUI.customLink.flyout.required": "必需", "xpack.apm.settings.customizeUI.customLink.flyout.save": "保存", "xpack.apm.settings.customizeUI.customLink.flyout.title": "创建链接", - "xpack.apm.settings.customizeUI.customLink.info": "这些链接将显示在事务的“操作”上下文菜单中。", "xpack.apm.settings.customizeUI.customLink.license.text": "要创建定制链接,必须订阅 Elastic 金级或更高许可证。使用上述许可证,将能够创建定制链接,以改善分析服务时的流程。", "xpack.apm.settings.customizeUI.customLink.linkPreview.descrition": "使用示例事务文档中的值基于上述筛选测试链接。", "xpack.apm.settings.customizeUI.customLink.preview.contextVariable.invalid": "由于定义的变量无效,我们无法找到示例事务文档。", @@ -5237,9 +5236,7 @@ "xpack.apm.transactionActionMenu.actionsButtonLabel": "操作", "xpack.apm.transactionActionMenu.container.subtitle": "查看此容器的日志和指标以获取进一步详情。", "xpack.apm.transactionActionMenu.container.title": "容器详情", - "xpack.apm.transactionActionMenu.customLink.popover.title": "定制链接", "xpack.apm.transactionActionMenu.customLink.section": "定制链接", - "xpack.apm.transactionActionMenu.customLink.seeMore": "查看更多内容", "xpack.apm.transactionActionMenu.customLink.subtitle": "链接将在新窗口打开。", "xpack.apm.transactionActionMenu.host.subtitle": "查看主机日志和指标以获取进一步详情。", "xpack.apm.transactionActionMenu.host.title": "主机详情", diff --git a/x-pack/test/reporting_api_integration/fixtures.ts b/x-pack/test/reporting_api_integration/fixtures.ts index c3448dada3a53..6faba20c94a9c 100644 --- a/x-pack/test/reporting_api_integration/fixtures.ts +++ b/x-pack/test/reporting_api_integration/fixtures.ts @@ -246,17 +246,18 @@ export const CSV_RESULT_NANOS_CUSTOM = `date,message,"_id" `; export const CSV_RESULT_DOCVALUE = `"order_date",category,currency,"customer_id","order_id","day_of_week_i","order_date","products.created_on",sku -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes""]",EUR,12,570552,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0216402164"",""ZO0666306663""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,34,570520,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0618906189"",""ZO0289502895""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,42,570569,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0643506435"",""ZO0646406464""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,45,570133,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0320503205"",""ZO0049500495""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories""]",EUR,4,570161,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0606606066"",""ZO0596305963""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,17,570200,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0025100251"",""ZO0101901019""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,27,732050,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0101201012"",""ZO0230902309"",""ZO0325603256"",""ZO0056400564""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,52,719675,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0448604486"",""ZO0686206862"",""ZO0395403954"",""ZO0528505285""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,26,570396,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0495604956"",""ZO0208802088""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Accessories""]",EUR,17,570037,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0321503215"",""ZO0200102001""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,26,569309,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0364103641"",""ZO0708807088""]" "Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,24,569311,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0024600246"",""ZO0660706607""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,31,569312,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0425104251"",""ZO0107901079""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Shoes""]",EUR,14,569336,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0512505125"",""ZO0384103841""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,28,569337,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0634106341"",""ZO0066900669""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories"",""Men's Clothing""]",EUR,31,569338,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0702507025"",""ZO0528105281""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,27,569356,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0010500105"",""ZO0172201722""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,19,569362,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0292402924"",""ZO0681006810""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,42,569370,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0358603586"",""ZO0641106411""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,20,569371,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0225702257"",""ZO0186601866""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,43,569375,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0347603476"",""ZO0668806688""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,48,569387,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0593805938"",""ZO0125201252""]" `; // This concatenates lines of multi-line string into a single line. diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts index ca3172807139c..20df601f2ff5c 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts @@ -355,7 +355,10 @@ export default function ({ getService }: FtrProviderContext) { timezone: 'UTC', }, state: { - sort: [{ order_date: { order: 'desc', unmapped_type: 'boolean' } }], + sort: [ + { order_date: { order: 'desc', unmapped_type: 'boolean' } }, + { order_id: { order: 'asc', unmapped_type: 'boolean' } }, + ], docvalue_fields: [ { field: 'customer_birth_date', format: 'date_time' }, { field: 'order_date', format: 'date_time' }, diff --git a/x-pack/test/reporting_api_integration/services.ts b/x-pack/test/reporting_api_integration/services.ts index 2c0252fde7693..3b908ecdd2b6e 100644 --- a/x-pack/test/reporting_api_integration/services.ts +++ b/x-pack/test/reporting_api_integration/services.ts @@ -5,8 +5,6 @@ */ import expect from '@kbn/expect'; -import * as Rx from 'rxjs'; -import { filter, first, mapTo, switchMap, timeout } from 'rxjs/operators'; import { indexTimestamp } from '../../plugins/reporting/server/lib/store/index_timestamp'; import { services as xpackServices } from '../functional/services'; import { services as apiIntegrationServices } from '../api_integration/services'; @@ -47,6 +45,7 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { const log = getService('log'); const supertest = getService('supertest'); const esSupertest = getService('esSupertest'); + const retry = getService('retry'); return { async waitForJobToFinish(downloadReportPath: string) { @@ -139,21 +138,12 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { log.debug('ReportingAPI.deleteAllReports'); // ignores 409 errs and keeps retrying - const deleted$ = Rx.interval(100).pipe( - switchMap(() => - esSupertest - .post('/.reporting*/_delete_by_query') - .send({ query: { match_all: {} } }) - .then(({ status }) => status) - ), - filter((status) => status === 200), - mapTo(true), - first(), - timeout(5000) - ); - - const reportsDeleted = await deleted$.toPromise(); - expect(reportsDeleted).to.be(true); + await retry.tryForTime(5000, async () => { + await esSupertest + .post('/.reporting*/_delete_by_query') + .send({ query: { match_all: {} } }) + .expect(200); + }); }, expectRecentPdfAppStats(stats: UsageStats, app: string, count: number) {