diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index 63798e2e29e44..650ef94e1d3da 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -11,8 +11,11 @@ kibanaPipeline(timeoutMinutes: 240) { 'CODE_COVERAGE=1', // Enables coverage. Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. ]) { workers.base(name: 'coverage-worker', size: 'l', ramDisk: false, bootstrapped: false) { - kibanaCoverage.runTests() - handleIngestion(TIME_STAMP) + catchError { + kibanaCoverage.runTests() + handleIngestion(TIME_STAMP) + } + handleFail() } } kibanaPipeline.sendMail() @@ -23,8 +26,19 @@ def handleIngestion(timestamp) { kibanaPipeline.downloadCoverageArtifacts() kibanaCoverage.prokLinks("### Process HTML Links") kibanaCoverage.collectVcsInfo("### Collect VCS Info") + kibanaCoverage.generateReports("### Merge coverage reports") + kibanaCoverage.uploadCombinedReports() kibanaCoverage.ingest(timestamp, '### Injest && Upload') kibanaCoverage.uploadCoverageStaticSite(timestamp) } +def handleFail() { + def buildStatus = buildUtils.getBuildStatus() + if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { + slackNotifications.sendFailedBuild( + channel: '#kibana-qa', + username: 'Kibana QA' + ) + } +} diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index d47ef93172a9d..11f9ccaeddb1e 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -40,7 +40,7 @@ echo "Creating bootstrap_cache archive" # archive cacheable directories mkdir -p "$HOME/.kibana/bootstrap_cache" tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ - x-pack/legacy/plugins/reporting/.chromium \ + x-pack/plugins/reporting/.chromium \ .es \ .chromedriver \ .geckodriver; diff --git a/.eslintrc.js b/.eslintrc.js index aeaf6e04fdc01..f0b7d6864bef0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -202,6 +202,11 @@ module.exports = { from: ['(src|x-pack)/plugins/*/server/**/*'], errorMessage: `Public code can not import from server, use a common directory.`, }, + { + target: ['(src|x-pack)/plugins/*/common/**/*'], + from: ['(src|x-pack)/plugins/*/(server|public)/**/*'], + errorMessage: `Common code can not import from server or public, use a common directory.`, + }, { target: [ '(src|x-pack)/legacy/**/*', @@ -220,7 +225,7 @@ module.exports = { '!src/core/server/index.ts', // relative import '!src/core/server/mocks{,.ts}', '!src/core/server/types{,.ts}', - '!src/core/server/test_utils', + '!src/core/server/test_utils{,.ts}', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 '!src/core/server/*.test.mocks{,.ts}', @@ -582,11 +587,11 @@ module.exports = { }, /** - * SIEM overrides + * Security Solution overrides */ { // front end typescript and javascript files only - files: ['x-pack/plugins/siem/public/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/security_solution/public/**/*.{js,ts,tsx}'], rules: { 'import/no-nodejs-modules': 'error', 'no-restricted-imports': [ @@ -600,7 +605,7 @@ module.exports = { }, { // typescript only for front and back end - files: ['x-pack/{,legacy/}plugins/siem/**/*.{ts,tsx}'], + files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{ts,tsx}'], rules: { // This will be turned on after bug fixes are complete // '@typescript-eslint/explicit-member-accessibility': 'warn', @@ -635,7 +640,7 @@ module.exports = { // { // // will introduced after the other warns are fixed // // typescript and javascript for front end react performance - // files: ['x-pack/plugins/siem/public/**/!(*.test).{js,ts,tsx}'], + // files: ['x-pack/plugins/security_solution/public/**/!(*.test).{js,ts,tsx}'], // plugins: ['react-perf'], // rules: { // // 'react-perf/jsx-no-new-object-as-prop': 'error', @@ -646,7 +651,7 @@ module.exports = { // }, { // typescript and javascript for front and back end - files: ['x-pack/{,legacy/}plugins/siem/**/*.{js,ts,tsx}'], + files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{js,ts,tsx}'], plugins: ['eslint-plugin-node', 'react'], env: { mocha: true, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 83f4f90b9204d..48d70910f9bf1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -37,6 +37,7 @@ /examples/url_generators_examples/ @elastic/kibana-app-arch /examples/url_generators_explorer/ @elastic/kibana-app-arch /packages/kbn-interpreter/ @elastic/kibana-app-arch +/packages/elastic-datemath/ @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch @@ -139,6 +140,7 @@ /config/kibana.yml @elastic/kibana-platform /x-pack/plugins/features/ @elastic/kibana-platform /x-pack/plugins/licensing/ @elastic/kibana-platform +/x-pack/plugins/global_search/ @elastic/kibana-platform /x-pack/plugins/cloud/ @elastic/kibana-platform /packages/kbn-config-schema/ @elastic/kibana-platform /src/legacy/server/config/ @elastic/kibana-platform @@ -225,12 +227,12 @@ /x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem -# SIEM -/x-pack/plugins/siem/ @elastic/siem @elastic/endpoint-app-team +# Security Solution +/x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team /x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/siem @elastic/siem @elastic/endpoint-app-team +/x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team /x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team /x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team # Security Intelligence And Analytics -/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics +/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/.node-version b/.node-version index 5b7269c0a98f3..b61c07ffddbd1 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -10.19.0 +10.21.0 diff --git a/.nvmrc b/.nvmrc index 5b7269c0a98f3..b61c07ffddbd1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.19.0 +10.21.0 diff --git a/.sass-lint.yml b/.sass-lint.yml index db895583eb8a7..eb43af293c670 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -4,7 +4,6 @@ files: - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' - 'src/plugins/vis_type_xy/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' - 'x-pack/plugins/lens/**/*.s+(a|c)ss' @@ -12,6 +11,7 @@ files: - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/spaces/**/*.s+(a|c)ss' + - 'x-pack/plugins/security/**/*.s+(a|c)ss' ignore: - 'x-pack/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' rules: diff --git a/Jenkinsfile b/Jenkinsfile index f435b18c6d824..b6a36c79f877d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -41,9 +41,10 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true) { 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-siemCypress': { processNumber -> - whenChanged(['x-pack/plugins/siem/', 'x-pack/test/siem_cypress/']) { - kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh')(processNumber) + 'xpack-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'), + 'xpack-securitySolutionCypress': { processNumber -> + whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) { + kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) } }, diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index f89f994e59e57..7b771eb662616 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -11,7 +11,7 @@ or, to only show transactions that are slower than a specified time threshold. ==== Example APM app queries * Exclude response times slower than 2000 ms: `transaction.duration.us > 2000000` -* Filter by response status code: `context.response.status_code >= 400` +* Filter by response status code: `context.response.status_code ≥ 400` * Filter by single user ID: `context.user.id : 12` When querying in the APM app, you're merely searching and selecting data from fields in Elasticsearch documents. diff --git a/docs/apm/deployment-annotations.asciidoc b/docs/apm/deployment-annotations.asciidoc index 9abcd9f6efc74..142b0c0193d74 100644 --- a/docs/apm/deployment-annotations.asciidoc +++ b/docs/apm/deployment-annotations.asciidoc @@ -7,13 +7,40 @@ ++++ For enhanced visibility into your deployments, we offer deployment annotations on all transaction charts. -This feature automatically tags new deployments, so you can easily see if your deploy has increased response times -for an end-user, or if the memory/CPU footprint of your application has changed. -Being able to identify bad deployments quickly enables you to rollback and fix issues without causing costly outages. +This feature enables you to easily determine if your deployment has increased response times for an end-user, +or if the memory/CPU footprint of your application has changed. +Being able to quickly identify bad deployments enables you to rollback and fix issues without causing costly outages. + +By default, automatic deployment annotations are enabled. +This means the APM app will create an annotation on your data when the `service.version` of your application changes. + +Alternatively, you can explicitly create deployment annotations with our annotation API. +The API can integrate into your CI/CD pipeline, +so that each time you deploy, a POST request is sent to the annotation API endpoint: + +[source,console] +---- +curl -X POST \ + http://localhost:5601/api/apm/services/${SERVICE_NAME}/annotation \ <1> +-H 'Content-Type: application/json' \ +-H 'kbn-xsrf: true' \ +-H 'Authorization: Basic ${API_KEY}' \ <2> +-d '{ + "@timestamp": "${DEPLOY_TIME}", <3> + "service": { + "version": "${SERVICE_VERSION}" <4> + }, + "message": "${MESSAGE}" <5> + }' +---- +<1> The `service.name` of your application +<2> An APM app API key with sufficient privileges +<3> The time of the deployment +<4> The `service.version` to be displayed in the annotation +<5> A custom message to be displayed in the annotation + +See the <> reference for more information. -Deployment annotations are enabled by default, and can be created with the <>. -If there are no created annotations for the selected time period, -the APM app will automatically annotate your data if the `service.version` of your application changes. NOTE: If custom annotations have been created for the selected time period, any derived annotations, i.e., those created automatically when `service.version` changes, will not be shown. diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index 3a6a96fca9d09..db2f85c54c762 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -62,9 +62,9 @@ Machine learning jobs can be created to calculate anomaly scores on APM transact When these jobs are active, service maps will display a color-coded anomaly indicator based on the detected anomaly score: [horizontal] -image:apm/images/green-service.png[APM green service]:: Max anomaly score **<=25**. Service is healthy. +image:apm/images/green-service.png[APM green service]:: Max anomaly score **≤25**. Service is healthy. image:apm/images/yellow-service.png[APM yellow service]:: Max anomaly score **26-74**. Anomalous activity detected. Service may be degraded. -image:apm/images/red-service.png[APM red service]:: Max anomaly score **>=75**. Anomalous activity detected. Service is unhealthy. +image:apm/images/red-service.png[APM red service]:: Max anomaly score **≥75**. Anomalous activity detected. Service is unhealthy. [role="screenshot"] image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] @@ -92,10 +92,10 @@ Type and subtype are based on `span.type`, and `span.subtype`. Service maps are supported for the following Agent versions: [horizontal] -Go Agent:: >= v1.7.0 -Java Agent:: >= v1.13.0 -.NET Agent:: >= v1.3.0 -Node.js Agent:: >= v3.6.0 -Python Agent:: >= v5.5.0 -Ruby Agent:: >= v3.6.0 -Real User Monitoring (RUM) Agent:: >= v4.7.0 +Go Agent:: ≥ v1.7.0 +Java Agent:: ≥ v1.13.0 +.NET Agent:: ≥ v1.3.0 +Node.js Agent:: ≥ v3.6.0 +Python Agent:: ≥ v5.5.0 +Ruby Agent:: ≥ v3.6.0 +Real User Monitoring (RUM) Agent:: ≥ v4.7.0 diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc index 4149039a3f87b..9c7467bb452fd 100644 --- a/docs/canvas/canvas-elements.asciidoc +++ b/docs/canvas/canvas-elements.asciidoc @@ -27,28 +27,30 @@ By default, most of the elements you create use demo data until you change the d * *Timelion* — Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>. +Each element can display a different data source. Pages and workpads often contain multiple data sources. + [float] [[canvas-add-object]] ==== Add a saved object -Add a <>, then customize it to fit your display needs. +Add <> to your workpad, such as maps and visualizations. -. Click *Embed object*. +. Click *Add element > Add from Visualize Library*. -. Select the object you want to add. +. Select the saved object you want to add. + [role="screenshot"] image::images/canvas-map-embed.gif[] . To use the customization options, click the panel menu, then select one of the following options: -* *Edit map* — Opens <> so that you can edit the original map. +* *Edit map* — Opens <> or <> so that you can edit the original saved object. -* *Customize panel* — Specifies the object title options. +* *Edit panel title* — Adds a title to the saved object. -* *Inspect* — Allows you to drill down into the element data. +* *Customize time range* — Exposes a time filter dedicated to the saved object. -* *Customize time range* — Exposes a time filter dedicated to the map. +* *Inspect* — Allows you to drill down into the element data. [float] [[canvas-add-image]] @@ -56,7 +58,7 @@ image::images/canvas-map-embed.gif[] To personalize your workpad, add your own logos and graphics. -. Click *Manage assets*. +. Click *Add element > Manage assets*. . On the *Manage workpad assets* window, drag and drop your images. @@ -83,40 +85,25 @@ Move and resize your elements to meet your design needs. [[format-canvas-elements]] ==== Format elements -Align, distribute, and reorder elements for consistency and readability across your workpad pages. - -Access the align, distribute, and reorder options by clicking the *Element options* icon. - -[role="screenshot"] -image::images/canvas_element_options.png[] +For consistency and readability across your workpad pages, align, distribute, and reorder elements. -To align elements: +To align two or more elements: . Press and hold Shift, then select the elements you want to align. -. Click the , then select *Group*. +. Click *Edit > Alignment*, then select the alignment option. -. Click the *Element options* icon, then select *Alignment*. - -. Select the alignment option. - -To distribute elements: +To distribute three or more elements: . Press and hold Shift, then select the elements you want to distribute. -. Click the *Element options* icon, then select *Group*. - -. Click the *Element options* icon, then select *Distribution*. - -. Select the distribution option. +. Click *Edit > Distribution*, then select the distribution option. To reorder elements: . Select the element you want to reorder. -. Click the *Element options* icon, then select *Order*. - -. Select the order option. +. Click *Edit > Order*, then select the order option. [float] [[data-display]] @@ -157,14 +144,14 @@ text.align: center; To use the elements across all workpads, save the elements. -When you're ready to save your element, select the element, then click the *Save as new element* icon. +When you're ready to save your element, select the element, then click *Edit > Save as new element*. [role="screenshot"] image::images/canvas_save_element.png[] -To save a group of elements, press and hold Shift, then select the elements you want to save. +To save a group of elements, press and hold Shift, select the elements you want to save, then click *Edit > Save as new element*. -To access your saved elements, click *Add element*, then select *My elements*. +To access your saved elements, click *Add element > My elements*. [float] [[delete-elements]] @@ -174,9 +161,7 @@ When you no longer need an element, delete it from your workpad. . Select the element you want to delete. -. Click the *Element options* icon. +. Click *Edit > Delete*. + [role="screenshot"] image::images/canvas_element_options.png[] - -. Select *Delete*. diff --git a/docs/canvas/canvas-present-workpad.asciidoc b/docs/canvas/canvas-present-workpad.asciidoc index 9cd4ecc9519e1..e0139ab943104 100644 --- a/docs/canvas/canvas-present-workpad.asciidoc +++ b/docs/canvas/canvas-present-workpad.asciidoc @@ -4,24 +4,20 @@ When you are ready to present your workpad, use and enable the presentation options. -[float] -[[view-fullscreen-mode]] -==== View your workpad in fullscreen mode +. Configure the autoplay options. -Click the *Enter fullscreen mode* icon. +.. From the workpad menu, click *View > Autoplay settings*. +.. Under *Change cycling interval*, select the interval you want to use, or *Set a custom interval*. ++ [role="screenshot"] -image::images/canvas-fullscreen.png[Fullscreen mode] - -[float] -[[enable-autoplay]] -==== Enable autoplay +image::images/canvas-autoplay-interval.png[Element autoplay interval] -Automatically cycle through your workpads pages in fullscreen mode. +. To enable autoplay, click *View > Turn autoplay on*. -. Click the *Control settings* icon. - -. Under *Change cycling interval*, select the interval you want to use. +. To start your presentation, click *View > Enter fullscreen mode*. + [role="screenshot"] -image::images/canvas-refresh-interval.png[Element data refresh interval] +image::images/canvas-fullscreen.png[Fullscreen mode] + +. When you are ready to exit fullscreen mode, press the Esc (Escape) key. diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index 5cae3fcc7b531..a095253c6cff3 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -10,14 +10,12 @@ When you've finished your workpad, you can share it outside of {kib}. Create a JSON file of your workpad that you can export outside of {kib}. -. From your workpad, click the *Share workpad* icon. +Click *Share > Download as JSON*. -. Select *Download as JSON*. -+ [role="screenshot"] image::images/canvas-export-workpad.png[Export single workpad] -Want to export multiple workpads? Go to the *Canvas workpads* view, select the workpads you want to export, then click *Export*. +Want to export multiple workpads? Go to the *Canvas* home page, select the workpads you want to export, then click *Export*. [float] [[create-workpad-pdf]] @@ -25,69 +23,43 @@ Want to export multiple workpads? Go to the *Canvas workpads* view, select the w If you have a license that supports the {report-features}, you can create a PDF copy of your workpad that you can save and share outside {kib}. -For more information, refer to <>. - -. From your workpad, click the *Share workpad* icon, then select *PDF reports*. +Click *Share > PDF reports > Generate PDF*. -. Click *Generate PDF*. -+ [role="screenshot"] image::images/canvas-generate-pdf.gif[Generate PDF] +For more information, refer to <>. + [float] [[create-workpad-URL]] ==== Create a POST URL If you have a license that supports the {report-features}, you can create a POST URL that you can use to automatically generate PDF reports using Watcher or a script. -For more information, refer to <>. - -. From your workpad, click the *Share workpad* icon, then select *PDF reports*. +Click *Share > PDF reports > Copy POST URL*. -. Click *Copy POST URL*. -+ [role="screenshot"] image::images/canvas-create-URL.gif[Create POST URL] +For more information, refer to <>. + [float] [[add-workpad-website]] ==== Share the workpad on a website beta[] Canvas allows you to create _shareables_, which are workpads that you download and securely share on any website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar. -. From your workpad, click the *Share this workpad* icon, then select *Share on a website*. +. Click *Share > Share on a website*. -. On the *Share on a website* pane, follow the instructions. +. Follow the *Share on a website* instructions. . To customize the workpad behavior to autoplay the pages or hide the toolbar, use the inline parameters. + To make sure that your data remains secure, the data in the JSON file is not connected to {kib}. Canvas does not display elements that manipulate the data on the workpad. + [role="screenshot"] -image::images/canvas-embed_workpad.gif[Share the workpad on a website] +image::canvas/images/canvas-embed_workpad.gif[Share the workpad on a website] + NOTE: Shareable workpads encode the current state of the workpad in a JSON file. When you make changes to the workpad, the changes do not appear in the shareable workpad on your website. -[float] -[[change-the-workpad-settings]] -==== Change the settings - -After you've added the workpad to your website, you can change the autoplay and toolbar settings. - -To change the autoplay settings: - -. Click the settings icon. - -. Click *Auto Play*, then change the settings. -+ -[role="screenshot"] -image::images/canvas_share_autoplay_480.gif[Autoplay settings] - -To change the toolbar settings: - -. Click the settings icon. - -. Click *Toolbar*, then change the settings. -+ -[role="screenshot"] -image::images/canvas_share_hidetoolbar_480.gif[Hide toolbar settings] +. To change the settings, click the settings icon, then choose the settings you want to use. diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index a38ab4a69598e..9b23817de2767 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -10,76 +10,64 @@ To get up and running with Canvas, use the following tutorial where you'll creat For this tutorial, you'll need to add the <>. [float] -=== Create and personalize your workpad +=== Create your workpad Your first step to working with Canvas is to create a workpad. -. Open *Canvas*. +. Open the menu, then click *Kibana > Canvas*. -. Click *Create workpad*. - -. To add a *Name* for your workpad, use the editor. For example, `My Canvas Workpad`. +. On the *Canvas workpads* page, click *Create workpad*. [float] === Customize your workpad with images To customize your workpad to look the way you want, add your own images. -. Click *Add element*, then click *Image*. +. Click *Add element > Image > Image*. + -The default Elastic logo image appears on your page. +The default Elastic logo image appears on the page. . To replace the Elastic logo with your own image, select the image, then use the editor. -. To move the image, click and drag it to your preferred location. - [role="screenshot"] image::images/canvas-image-element.png[] -You'll notice that the image is tagged as an asset, which allows you to reuse the image from *Manage assets*. - [float] === Customize your data with metrics Customize your data by connecting it to the Sample eCommerce orders data. -. Click *Add element*, then click *Metric*. +. Click *Add element > Chart > Metric*. + -By default, the *Metric* element is connected to a demo data source, which enables you to experiment with the element before you connect it to your own data source. - -. To connect the element to your own data source, make sure that the element is selected, then click *Data*. +By default, the element is connected to the demo data, which enables you to experiment with the element before you connect it to your own data source. -.. Click *Change your data source*, then click *Elasticsearch SQL*. +. To connect the element to your own data source, make sure that the element is selected, click *Data > Demo data > Elasticsearch SQL*. -.. In the *Elasticsearch SQL query* field, enter the following query: +.. In the *Query* field, enter the following: + `SELECT sum(taxless_total_price) AS sum_total_price FROM "kibana_sample_data_ecommerce"` -+ -The query selects the total price field and sets it to the sum_total_price field. These fields are pulled from the kibana_sample_data_ecommerce index that you installed. -.. To verify that the data is correct, click *Preview*. If you like what you see, click *Save*. +.. Click *Save*. + -At this point, the element displays an error. +The query selects the total price field and sets it to the sum_total_price field. All fields are pulled from the kibana_sample_data_ecommerce index. -. Specify how to process and display the data. +. At this point, the element appears as an error, so you need to change the element display options. .. Click *Display* -.. Under *Number*, select *Value* from the function drop-down list, then select *sum_total_price* from the column drop-down list. +.. From the *Value* drop-down lists, make sure that *Unique* is selected, then select *sum_total_price*. .. Change the *Label* to `Total sales`. -+ -You'll notice that the error is gone, but the number could use some formatting. -. To format the number, use the Canvas expression language. +. The error is gone, but the element could use some formatting. To format the number, use the Canvas expression language. .. Click *Expression editor*. + You're now looking at the raw data syntax that Canvas uses to display the element. -.. Look for `math "sum_total_price"`, then add `| formatNumber "$0a"`. +.. Change `metricFormat="0,0.[000]"` to `metricFormat="$0a"`. -.. To update the number, click *Run*. +.. Click *Run*. [role="screenshot"] image::images/canvas-metric-element.png[] @@ -89,21 +77,17 @@ image::images/canvas-metric-element.png[] To show what your data can do, add charts, graphs, progress monitors, and more to your workpad. -. Click *Add element*, then click *Area chart*. +. Click *Add element > Chart > Area*. -. To connect the element to your own data source, make sure that the element is selected, then click *Data*. +. Make sure that the element is selected, then click *Data > Demo data > Elasticsearch SQL*. -.. Click *Change your data source*, then click *Elasticsearch SQL*. - -.. To obtain the taxless total price by date, enter the following into the *Elasticsearch SQL query* field: +.. To obtain the taxless total price by date, enter the following in the *Query* field: + `SELECT order_date, taxless_total_price FROM "kibana_sample_data_ecommerce" ORDER BY order_date` -+ -Although you used the Elasticsearch SQL data source for the metric and area chart elements, each element can display a different data source. Pages and workpads often contain multiple data sources. -.. To verify that the data is correct, click *Preview*. If you like what you see, click *Save*. +.. Click *Save*. -. Specify how to display the data. +. Change the display options. .. Click *Display* @@ -117,34 +101,20 @@ image::images/canvas-chart-element.png[] [float] === Show how your data changes over time -To focus your data on a specific time range, add a time filter to your workpad. +To focus your data on a specific time range, add the time filter. -. Click *Add element*, then click *Time filter*. +. Click *Add element > Filter > Time filter*. -. Specify how to display the data. +. Click *Display* -.. Click *Display* - -.. To use the date time field from the sample data, enter `order_date` in the *Column* field, then click *Set*. +. To use the date time field from the sample data, enter `order_date` in the *Column* field, then click *Set*. [role="screenshot"] image::images/canvas-timefilter-element.png[] -To see how the data changes, set the time filter to *Last 7 days*. As you change the time filter options, the metrics dynamically update. - -Your workpad is now complete! From the workpad menu, use the icons to: - -* Configure the refresh rate for your data - -* Refresh the data that displays on your workpad - -* Display your workpad in fullscreen mode - -* Control the zoom options - -* Share your workpad +To see how the data changes, set the time filter to *Last 7 days*. As you change the time filter options, the elements automatically update. -* Hide the editing controls +Your workpad is now complete! [float] === Next steps diff --git a/docs/canvas/canvas-workpad.asciidoc b/docs/canvas/canvas-workpad.asciidoc index 42eedf55c404d..ac2d348920114 100644 --- a/docs/canvas/canvas-workpad.asciidoc +++ b/docs/canvas/canvas-workpad.asciidoc @@ -20,9 +20,7 @@ To create a workpad, choose one of the following options: To use the background colors, images, and data of your choice, start with a blank workpad. -. Open *Canvas*. - -. On the *Canvas workpads* view, click *Create workpad*. +. On the *Canvas workpads* page, click *Create workpad*. . Add a *Name* to your workpad. @@ -35,7 +33,7 @@ For example, click *720p* for a traditional presentation layout. . Click the *Background color* picker, then select the background color for your workpad. + [role="screenshot"] -image::images/canvas-background-color-picker.gif[Canvas color picker] +image::images/canvas-background-color-picker.png[Canvas color picker] [float] [[canvas-template-workpad]] @@ -43,9 +41,7 @@ image::images/canvas-background-color-picker.gif[Canvas color picker] If you're unsure about where to start, you can use one of the preconfigured templates that come with Canvas. -. Open *Canvas*. - -. On the *Canvas workpads* view, select *Templates*. +. On the *Canvas workpads* page, select *Templates*. . Click the preconfigured template that you want to use. @@ -57,9 +53,7 @@ If you're unsure about where to start, you can use one of the preconfigured temp When you want to use a workpad that someone else has already started, import the JSON file into Canvas. -. Open *Canvas*. - -. On the *Canvas workpads* view, click and drag the file to the *Import workpad JSON file* field. +To import a workpad, go to the *Canvas workpads* page, then click and drag the file to the *Import workpad JSON file* field. [float] [[sample-data-workpad]] @@ -96,23 +90,27 @@ background-color: #3990e6; [[configure-auto-refresh-interval]] === Change the auto-refresh interval -Increase or decrease how often the data refreshes on your workpad. +Change how often the data refreshes on your workpad. -. In the top left corner, click the *Control settings* icon. +. Click *View > Auto refresh settings*. -. Under *Change auto-refresh interval*, select the interval you want to use. +. Select the interval you want to use, or *Set a custom interval*. + [role="screenshot"] image::images/canvas-refresh-interval.png[Element data refresh interval] - -TIP: To manually refresh the data, click the *Refresh data* icon. ++ +To manually refresh the data, click image:canvas/images/canvas-refresh-data.png[]. [float] [[zoom-in-out]] === Use the zoom options -In the upper left corner, click the *Zoom controls* icon, then select one of the options. +To get a closer look at a portion of your workpad, use the zoom options. + +. Click *View > Zoom*. +. Select the zoom option. ++ [role="screenshot"] image::images/canvas-zoom-controls.png[Zoom controls] diff --git a/docs/canvas/images/canvas-embed_workpad.gif b/docs/canvas/images/canvas-embed_workpad.gif new file mode 100644 index 0000000000000..1cda5b572acef Binary files /dev/null and b/docs/canvas/images/canvas-embed_workpad.gif differ diff --git a/docs/canvas/images/canvas-refresh-data.png b/docs/canvas/images/canvas-refresh-data.png new file mode 100644 index 0000000000000..7a71686f04491 Binary files /dev/null and b/docs/canvas/images/canvas-refresh-data.png differ diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md index 7f72d6a52fc2a..e898126a553e2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md @@ -23,10 +23,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Route } from 'react-router-dom'; -import { CoreStart, AppMountParams } from 'src/core/public'; +import { CoreStart, AppMountParameters } from 'src/core/public'; import { MyPluginDepsStart } from './plugin'; -export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { +export renderApp = ({ element, history, onAppLeave }: AppMountParameters) => { const { renderApp, hasUnsavedChanges } = await import('./application'); onAppLeave(actions => { if(hasUnsavedChanges()) { diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 02cc34baf7c45..75d3abefc74b9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -114,6 +114,7 @@ | [SearchBar](./kibana-plugin-plugins-data-public.searchbar.md) | | | [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.sync_search_strategy.md) | | | [syncQueryStateWithUrl](./kibana-plugin-plugins-data-public.syncquerystatewithurl.md) | Helper to setup syncing of global data with the URL | +| [UI\_SETTINGS](./kibana-plugin-plugins-data-public.ui_settings.md) | | ## Type Aliases diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md index 5ec2d491295bf..64108a7c7be33 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md @@ -9,12 +9,12 @@ Constructs a new instance of the `DataPublicPlugin` class Signature: ```typescript -constructor(initializerContext: PluginInitializerContext); +constructor(initializerContext: PluginInitializerContext); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| initializerContext | PluginInitializerContext | | +| initializerContext | PluginInitializerContext<ConfigSchema> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.md index 6cbc1f441c048..0dad92a0a27ca 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.md @@ -7,14 +7,14 @@ Signature: ```typescript -export declare function plugin(initializerContext: PluginInitializerContext): DataPublicPlugin; +export declare function plugin(initializerContext: PluginInitializerContext): DataPublicPlugin; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| initializerContext | PluginInitializerContext | | +| initializerContext | PluginInitializerContext<ConfigSchema> | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md new file mode 100644 index 0000000000000..a48f4920b3d26 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [UI\_SETTINGS](./kibana-plugin-plugins-data-public.ui_settings.md) + +## UI\_SETTINGS variable + +Signature: + +```typescript +UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +} +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.config.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.config.md new file mode 100644 index 0000000000000..49b5f6040fc84 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.config.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [config](./kibana-plugin-plugins-data-server.config.md) + +## config variable + +Signature: + +```typescript +config: PluginConfigDescriptor +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index e756eb9b72905..0efbe8ed4ed64 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -55,12 +55,14 @@ | Variable | Description | | --- | --- | | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-server.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | +| [config](./kibana-plugin-plugins-data-server.config.md) | | | [esFilters](./kibana-plugin-plugins-data-server.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-server.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-server.esquery.md) | | | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | | [search](./kibana-plugin-plugins-data-server.search.md) | | +| [UI\_SETTINGS](./kibana-plugin-plugins-data-server.ui_settings.md) | | ## Type Aliases diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin._constructor_.md index 454d8a059a252..4a0a159310b9d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin._constructor_.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin._constructor_.md @@ -9,12 +9,12 @@ Constructs a new instance of the `DataServerPlugin` class Signature: ```typescript -constructor(initializerContext: PluginInitializerContext); +constructor(initializerContext: PluginInitializerContext); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| initializerContext | PluginInitializerContext | | +| initializerContext | PluginInitializerContext<ConfigSchema> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.md index b3ba75ce29ab6..1773871d946a2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.md @@ -9,14 +9,14 @@ Static code to be shared externally Signature: ```typescript -export declare function plugin(initializerContext: PluginInitializerContext): DataServerPlugin; +export declare function plugin(initializerContext: PluginInitializerContext): DataServerPlugin; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| initializerContext | PluginInitializerContext | | +| initializerContext | PluginInitializerContext<ConfigSchema> | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md new file mode 100644 index 0000000000000..855cfd11d00ea --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [UI\_SETTINGS](./kibana-plugin-plugins-data-server.ui_settings.md) + +## UI\_SETTINGS variable + +Signature: + +```typescript +UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +} +``` diff --git a/docs/images/add-data-fv.png b/docs/images/add-data-fv.png new file mode 100755 index 0000000000000..45313d133822c Binary files /dev/null and b/docs/images/add-data-fv.png differ diff --git a/docs/images/add-data-tutorials.png b/docs/images/add-data-tutorials.png new file mode 100644 index 0000000000000..74deedc57b42e Binary files /dev/null and b/docs/images/add-data-tutorials.png differ diff --git a/docs/images/canvas-add-image.gif b/docs/images/canvas-add-image.gif index a2263e22c4c49..994ec6e1b4f28 100644 Binary files a/docs/images/canvas-add-image.gif and b/docs/images/canvas-add-image.gif differ diff --git a/docs/images/canvas-add-pages.gif b/docs/images/canvas-add-pages.gif index a1fa228645836..c6e09d6f386ae 100644 Binary files a/docs/images/canvas-add-pages.gif and b/docs/images/canvas-add-pages.gif differ diff --git a/docs/images/canvas-autoplay-interval.png b/docs/images/canvas-autoplay-interval.png new file mode 100644 index 0000000000000..68a7ca248d9ee Binary files /dev/null and b/docs/images/canvas-autoplay-interval.png differ diff --git a/docs/images/canvas-background-color-picker.png b/docs/images/canvas-background-color-picker.png new file mode 100644 index 0000000000000..ec38b5c1c5f7e Binary files /dev/null and b/docs/images/canvas-background-color-picker.png differ diff --git a/docs/images/canvas-chart-element.png b/docs/images/canvas-chart-element.png index d0aa7db375a40..bf5e04bf89af5 100644 Binary files a/docs/images/canvas-chart-element.png and b/docs/images/canvas-chart-element.png differ diff --git a/docs/images/canvas-create-URL.gif b/docs/images/canvas-create-URL.gif index 0c9fbf7201d80..11327224fc897 100644 Binary files a/docs/images/canvas-create-URL.gif and b/docs/images/canvas-create-URL.gif differ diff --git a/docs/images/canvas-element-select.gif b/docs/images/canvas-element-select.gif index bd0e49377262e..1bfd1132f25c7 100644 Binary files a/docs/images/canvas-element-select.gif and b/docs/images/canvas-element-select.gif differ diff --git a/docs/images/canvas-export-workpad.png b/docs/images/canvas-export-workpad.png index fa910daf948d7..213bbaa5a26d3 100644 Binary files a/docs/images/canvas-export-workpad.png and b/docs/images/canvas-export-workpad.png differ diff --git a/docs/images/canvas-fullscreen.png b/docs/images/canvas-fullscreen.png index 7e6ec6ad7e7a8..b8a816d290396 100644 Binary files a/docs/images/canvas-fullscreen.png and b/docs/images/canvas-fullscreen.png differ diff --git a/docs/images/canvas-generate-pdf.gif b/docs/images/canvas-generate-pdf.gif index 9ef16dc1e5017..513f6b3b960f9 100644 Binary files a/docs/images/canvas-generate-pdf.gif and b/docs/images/canvas-generate-pdf.gif differ diff --git a/docs/images/canvas-image-element.png b/docs/images/canvas-image-element.png index f869ccb344a46..13c9090e77c76 100644 Binary files a/docs/images/canvas-image-element.png and b/docs/images/canvas-image-element.png differ diff --git a/docs/images/canvas-map-embed.gif b/docs/images/canvas-map-embed.gif index 59ef97e0ceae8..c6ba5c29df42a 100644 Binary files a/docs/images/canvas-map-embed.gif and b/docs/images/canvas-map-embed.gif differ diff --git a/docs/images/canvas-metric-element.png b/docs/images/canvas-metric-element.png index d9735a2fb3e91..03871dcc81862 100644 Binary files a/docs/images/canvas-metric-element.png and b/docs/images/canvas-metric-element.png differ diff --git a/docs/images/canvas-refresh-interval.png b/docs/images/canvas-refresh-interval.png index 99006a5b8f12d..62e88ad4bf7d0 100644 Binary files a/docs/images/canvas-refresh-interval.png and b/docs/images/canvas-refresh-interval.png differ diff --git a/docs/images/canvas-timefilter-element.png b/docs/images/canvas-timefilter-element.png index 8b8356ff5f7ea..e210b0b3288c6 100644 Binary files a/docs/images/canvas-timefilter-element.png and b/docs/images/canvas-timefilter-element.png differ diff --git a/docs/images/canvas-zoom-controls.png b/docs/images/canvas-zoom-controls.png index 892721b627027..5c72d118041e4 100644 Binary files a/docs/images/canvas-zoom-controls.png and b/docs/images/canvas-zoom-controls.png differ diff --git a/docs/images/canvas_element_options.png b/docs/images/canvas_element_options.png index 191348d919b50..41457bab4ff36 100644 Binary files a/docs/images/canvas_element_options.png and b/docs/images/canvas_element_options.png differ diff --git a/docs/images/canvas_save_element.png b/docs/images/canvas_save_element.png index a63f5135f2a0e..8a601efab874a 100644 Binary files a/docs/images/canvas_save_element.png and b/docs/images/canvas_save_element.png differ diff --git a/docs/infrastructure/infra-ui.asciidoc b/docs/infrastructure/infra-ui.asciidoc index 120a22541717c..96550b4ed5758 100644 --- a/docs/infrastructure/infra-ui.asciidoc +++ b/docs/infrastructure/infra-ui.asciidoc @@ -109,5 +109,5 @@ Depending on the features you have installed and configured, you may also be abl * Select *View APM* to <> in the *APM* app. -* Select *View Uptime* to <> in the *Uptime* app. +* Select *View Uptime* to {uptime-guide}/uptime-app-overview.html[view uptime information] in the *Uptime* app. diff --git a/docs/ingest_manager/index.asciidoc b/docs/ingest_manager/index.asciidoc index 866935d1fa580..1728309f3dfd9 100644 --- a/docs/ingest_manager/index.asciidoc +++ b/docs/ingest_manager/index.asciidoc @@ -110,12 +110,12 @@ fetched by this input should be processed and which Data Stream to send it to. Ingest Management enforces an indexing strategy to allow the system to automatically detect indices and run queries on it. In short the indexing strategy looks as following: ``` -{type}-{dataset}-{namespace} +{dataset.type}-{dataset.name}-{dataset.namespace} ``` -The `{type}` can be `logs` or `metrics`. The `{namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`. +The `{dataset.type}` can be `logs` or `metrics`. The `{dataset.namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`. -Note: More `{type}`s might be added in the future like `apm` and `endpoint`. +Note: More `{dataset.type}`s might be added in the future like `traces`. This indexing strategy has a few advantages: @@ -133,7 +133,7 @@ Overall it creates smaller indices in size, makes querying more efficient and al The ingest pipelines for a specific dataset will have the following naming scheme: ``` -{type}-{dataset}-{package.version} +{dataset.type}-{dataset.name}-{package.version} ``` As an example, the ingest pipeline for the Nginx access logs is called `logs-nginx.access-3.4.1`. The same ingest pipeline is used for all namespaces. It is possible that a dataset has multiple ingest pipelines in which case a suffix is added to the name. @@ -151,7 +151,7 @@ Each type template contains an ILM policy. Modifying this default ILM policy wil The templates for a dataset are called as following: ``` -{type}-{dataset} +{dataset.type}-{dataset.name} ``` The pattern used inside the index template is `{type}-{dataset}-*` to match all namespaces. diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index 4f5992f945c7a..eb3025f88ce1b 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -96,5 +96,5 @@ When the machine learning anomaly detection features are enabled, click *Log rat To see other actions related to the event, click *Actions* in the log event details. Depending on the event and the features you have configured, you may also be able to: -* Select *View status in Uptime* to <> in the *Uptime* app. +* Select *View status in Uptime* to {uptime-guide}/uptime-app-overview.html[view related uptime information] in the *Uptime* app. * Select *View in APM* to <> in the *APM* app. diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc index 00ec5c7d2ddea..51d9f42a0b83e 100644 --- a/docs/management/managing-remote-clusters.asciidoc +++ b/docs/management/managing-remote-clusters.asciidoc @@ -31,6 +31,10 @@ to reproduce indices in the remote cluster on a local cluster. [role="screenshot"] image::images/add_remote_cluster.png[][UI for adding a remote cluster] +To create an index pattern to search across clusters, +use the same syntax that you’d use in a raw cross-cluster search request in {es}: :. +See <> for examples. + [float] [[manage-remote-clusters]] === Manage remote clusters diff --git a/docs/maps/images/fu_gs_select_source_file_upload.png b/docs/maps/images/fu_gs_select_source_file_upload.png index 6939f6a82b297..4fe1162acb29c 100644 Binary files a/docs/maps/images/fu_gs_select_source_file_upload.png and b/docs/maps/images/fu_gs_select_source_file_upload.png differ diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 00acb73bd276f..6137e028db3fd 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -1,44 +1,105 @@ [[connect-to-elasticsearch]] -== Connect Kibana with Elasticsearch +== Adding data -Before you can start using Kibana, you need to tell it which Elasticsearch indices you want to explore. -The first time you access Kibana, you are prompted to define an _index pattern_ that matches the name of -one or more of your indices. That's it. That's all you need to configure to start using Kibana. You can -add index patterns at any time from the <>. +To start working with your data in {kib}, you can: -TIP: By default, Kibana connects to the Elasticsearch instance running on `localhost`. To connect to a -different Elasticsearch instance, modify the Elasticsearch URL in the `kibana.yml` configuration file and -restart Kibana. For information about using Kibana with your production nodes, see <>. +* Upload a CSV, JSON, or log file with the File Data Visualizer. -To configure the Elasticsearch indices you want to access with Kibana: +* Upload geospatial data with the GeoJSON Upload feature. -. Point your browser at port 5601 to access the Kibana UI. For example, `localhost:5601` or -`http://YOURDOMAIN.com:5601`. -+ -image:images/Start-Page.png[Kibana start page] -+ -. Specify an index pattern that matches the name of one or more of your Elasticsearch indices. The pattern -can include an asterisk (*) to matches zero or more characters in an index's name. When filling out your -index pattern, any matched indices will be displayed. -. Click *Next Step* to select the index field that contains the timestamp you want to use to perform time-based -comparisons. Kibana reads the index mapping to list all of the fields that contain a timestamp. If your -index doesn't have time-based data, choose *I don't want to use the Time Filter* option. -+ -. Click *Create index pattern* to add the index pattern. This first pattern is automatically configured as the default. -When you have more than one index pattern, you can designate which one to use as the default by clicking -on the star icon above the index pattern title from *Management > Index Patterns*. +* Index logs, metrics, events, or application data by setting up a Beats module. + +* Connect {kib} with existing {es} indices. + +If you're not ready to use your own data, you can add a <> +to see all that you can do in {kib}. + +[float] +[[upload-data-kibana]] +=== Upload a CSV, JSON, or log file + +To visualize data in a CSV, JSON, or log file, you can +upload it using the File Data Visualizer. On the home page, +click *Import a CSV, NDSON, or log file*, and then drag your file into the +File Data Visualizer. + +You can upload a file up to 100 MB. This value is configurable up to 1 GB in +<>. + +[role="screenshot"] +image::images/add-data-fv.png[File Data Visualizer] + +The File Data Visualizer uses the {ref}/ml-find-file-structure.html[find_file_structure API] to analyze +the uploaded file and to suggest ingest pipelines and mappings for your data. + +NOTE: This feature is not intended for use as part of a +repeated production process, but rather for the initial exploration of your data. + +[float] +[[upload-geoipdata-kibana]] +=== Upload geospatial data + +To visualize geospatial data in a point or shape file, you can upload it using the <> +feature in *Elastic Maps*, and then use that data as a layer in a map. +The data is also available for use in the broader Kibana ecosystem, for example, +in visualizations and Canvas workpads. +With GeoJSON Upload, you can upload a file up to 50 MB. + +[role="screenshot"] +image::images/fu_gs_select_source_file_upload.png[] -All done! Kibana is now connected to your Elasticsearch data. Kibana displays a read-only list of fields -configured for the matching index. [float] -[[explore]] -=== Start Exploring your Data! -You're ready to dive in to your data: +[[add-data-tutorial-kibana]] +=== Index metrics, log, security, and application data -* Search and browse your data interactively from the <> page. -* Chart and map your data from the <> page. -* Create and view custom dashboards from the <> page. +The built-in data tutorials can help you quickly get up and running with +metrics data, log analytics, security events, and application data. +These tutorials walk you through installing and configuring a +Beats data shipper to periodically collect and send data to {es}. +You can then use the pre-built dashboards to explore and analyze the data. -For a step-by-step introduction to these core Kibana concepts, see the <> tutorial. +You access the tutorials from the home page. +If a tutorial doesn’t exist for your data, go to the {beats-ref}/beats-reference.html[Beats overview] +to learn about other data shippers in the Beats family. + +[role="screenshot"] +image::images/add-data-tutorials.png[Add Data tutorials] + + +[float] +[[connect-to-es]] +=== Connect with {es} indices + +To visualize data in existing {es} indices, you must +create an index pattern that matches the names of the indices that you want to explore. +When you add data with the File Data Visualizer, GeoJSON Upload feature, +or built-in tutorial, an index pattern is created for you. + +. Go to *Stack Management*, and then click *Index Patterns*. + +. Click *Create index pattern*. + +. Specify an index pattern that matches the name of one or more of your Elasticsearch indices. ++ +For example, an index pattern can point to your Apache data from yesterday, +`filebeat-apache-4-3-2022`, or any index that matches the pattern, `filebeat-*`. +Using a wildcard is the more popular approach. + + +. Click *Next Step*, and then select the index field that contains the timestamp you want to use to perform time-based +comparisons. ++ +Kibana reads the index mapping and lists all fields that contain a timestamp. If your +index doesn't have time-based data, choose *I don't want to use the Time Filter*. ++ +You must select a time field to use global time filters on your dashboards. + +. Click *Create index pattern*. ++ +{kib} is now configured to access your {es} indices. +You’ll see a list of fields configured for the matching index. +You can designate your index pattern as the default by clicking the star icon on this page. ++ +When searching in *Discover* and creating visualizations, you choose a pattern +from the index pattern menu to specify the {es} indices that contain the data you want to explore. diff --git a/docs/uptime-guide/alerting.asciidoc b/docs/uptime-guide/alerting.asciidoc new file mode 100644 index 0000000000000..bf9e7693fc7a5 --- /dev/null +++ b/docs/uptime-guide/alerting.asciidoc @@ -0,0 +1,33 @@ +[role="xpack"] +[[uptime-alerting]] + +=== Uptime alerting + +The Uptime app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] +feature. It provides a set of built-in actions and Uptime specific threshold alerts for you to use +and enables central management of all alerts from {kibana-ref}/management.html[Kibana Management]. + +[role="screenshot"] +image::images/create-alert.png[Create alert] + +[float] +==== Monitor status alerts + +To receive alerts when a monitor goes down, use the alerting menu at the top of the +overview page. Use a query in the alert flyout to determine which monitors to check +with your alert. If you already have a query in the overview page search bar it will +be carried over into this box. + +[role="screenshot"] +image::images/monitor-status-alert.png[Create monitor status alert flyout] + +[float] +==== TLS alerts + +Uptime also provides the ability to create an alert that will notify you when one or +more of your monitors have a TLS certificate that will expire within some threshold, +or when its age exceeds a limit. The values for these thresholds are configurable on +the <>. + +[role="screenshot"] +image::images/tls-alert.png[Create TLS alert flyout] diff --git a/docs/uptime-guide/app-overview.asciidoc b/docs/uptime-guide/app-overview.asciidoc new file mode 100644 index 0000000000000..692489a7ad311 --- /dev/null +++ b/docs/uptime-guide/app-overview.asciidoc @@ -0,0 +1,70 @@ +[role="xpack"] +[[uptime-app]] +== Uptime app + +The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. +You can explore endpoint status over time, drill down into specific monitors, +and view a high-level snapshot of your environment at any point in time. + +[role="screenshot"] +image::images/uptime-overview.png[Uptime app overview] + +[role="xpack"] +[[uptime-app-overview]] +=== Overview + +The Uptime overview helps you quickly identify and diagnose outages and +other connectivity issues within your network or environment. You can use the date range +selection that is global to the Uptime app, to highlight +an absolute date range, or a relative one, similar to other areas of {kib}. + +[float] +=== Filter bar + +The Filter bar enables you to quickly view specific groups of monitors, or even +an individual monitor if you have defined many. + +This control allows you to use automated filter options, as well as input custom filter +text to select specific monitors by field, URL, ID, and other attributes. + +[role="screenshot"] +image::images/filter-bar.png[Filter bar] + +[float] +=== Snapshot panel + +The Snapshot panel displays the overall +status of the environment you're monitoring or a subset of those monitors. +You can see the total number of detected monitors within the selected +Uptime date range, along with the number of monitors +in an `up` or `down` state, which is based on the last check reported by Heartbeat +for each monitor. + +Next to the counts, there is a histogram displaying the change over time throughout the +selected date range. + +[role="screenshot"] +image::images/snapshot-view.png[Snapshot view] + +[float] +=== Monitor list + +Information about individual monitors is displayed in the monitor list and provides a quick +way to navigate to a more in-depth visualization for interesting hosts or endpoints. + +The information displayed includes the recent status of a host or endpoint, when the monitor was last checked, its +ID and URL, and its IP address. There is also sparkline showing its check status over time. + +[role="screenshot"] +image::images/monitor-list.png[Monitor list] + +[float] +=== Observability integrations + +The Monitor list also contains a menu of available integrations. When Uptime detects Kubernetes or +Docker related host information, it provides links to open the Metrics app or Logs app pre-filtered +for this host. Additionally, to help you quickly determine if these solutions contain data relevant to you, +this feature contains links to filter the other views on the host's IP address. + +[role="screenshot"] +image::images/observability_integrations.png[Observability integrations] diff --git a/docs/uptime-guide/certificates.asciidoc b/docs/uptime-guide/certificates.asciidoc new file mode 100644 index 0000000000000..58db91aa080eb --- /dev/null +++ b/docs/uptime-guide/certificates.asciidoc @@ -0,0 +1,15 @@ +[role="xpack"] +[[uptime-certificates]] + +=== Certificates + +The certificates page enables you to visualize TLS certificate data in your indices. In addition to the +common name, associated monitors, issuer information, and SHA fingerprints, Uptime also assigns a status +derived from the threshold values in the <>. + +Several of the columns on this page are sortable. You can use the search bar at the top of the view +to find values in most of the TLS-related fields in your Uptime indices. Additionally, using the `Alerts` +dropdown at the top of the page you can create a TLS alert. + +[role="screenshot"] +image::images/certificates-page.png[Certificates] diff --git a/docs/uptime-guide/deployment-arch.asciidoc b/docs/uptime-guide/deployment-arch.asciidoc index d8edf290b9a5e..c1b2f596c6665 100644 --- a/docs/uptime-guide/deployment-arch.asciidoc +++ b/docs/uptime-guide/deployment-arch.asciidoc @@ -4,22 +4,24 @@ There are multiple ways to deploy Uptime and Heartbeat. Use the information in this section to determine the best deployment for you. -A guiding principle is that an outage that takes down the service being monitored should not also take down Heartbeat. -You want Heartbeat to be functioning even when your service is not, so the guidelines here help you maximise this possibility. +A guiding principle is that when an outage takes down the service being monitored it should not also take down Heartbeat. +You want Heartbeat to be functioning even when your service is not, so the guidelines here help you maximize this possibility. -Heartbeat is generally run as a centralized service within a data center. +Heartbeat is commonly run as a centralized service within a data center. While it is possible to run it as a separate "sidecar" process paired with each process/container, we recommend against it. Running Heartbeat centrally ensures you will still be able to see monitoring data in the event of an overloaded, disconnected, or otherwise malfunctioning server. -For further redundancy, you may want to deploy multiple Heartbeats across geographic and/or network boundaries to provide more data. - Specify Heartbeat's observer {heartbeat-ref}/configuration-observer-options.html[geo options] to do so. Some examples might be: +For further redundancy, you may want to deploy multiple Heartbeats across geographic and network boundaries to provide more data. +To do so, specify Heartbeat's observer {heartbeat-ref}/configuration-observer-options.html[geo options]. + +Some examples might be: * **A site served from a content delivery network (CDN) with points of presence (POPs) around the globe:** -In this case you may want to have multiple Heartbeat instances at different data centers around the world checking to see if your site is reachable via local CDN POPs. +To check if your site is reachable via CDN POPS, you may want to have multiple Heartbeat instances at different data centers around the world. * **A service within a single data center that is accessed across multiple VPNs:** Set up one Heartbeat instance within the VPN the service operates from, and another within an additional VPN that users access the service from. -Having both instances will help pinpoint network errors in the event of an outage. +Having both instances helps pinpoint network errors in the event of an outage. * **A single service running primarily in a US east coast data center, with a hot failover located in a US west coast data center:** In each data center, run a Heartbeat instance that checks both the local copy of the service and its counterpart across the country. Set up two monitors in each region, one for the local service and one for the remote service. -In the event of a data center failure it will be immediately obvious if the service had a connectivity issue to the outside world or if the failure was only internal. +In the event of a data center failure it will be immediately apparent if the service had a connectivity issue to the outside world or if the failure was only internal. diff --git a/docs/uptime-guide/images/cert-exp.png b/docs/uptime-guide/images/cert-exp.png new file mode 100644 index 0000000000000..cd87668db96dd Binary files /dev/null and b/docs/uptime-guide/images/cert-exp.png differ diff --git a/docs/uptime/images/certificates-page.png b/docs/uptime-guide/images/certificates-page.png similarity index 100% rename from docs/uptime/images/certificates-page.png rename to docs/uptime-guide/images/certificates-page.png diff --git a/docs/uptime/images/check-history.png b/docs/uptime-guide/images/check-history.png similarity index 100% rename from docs/uptime/images/check-history.png rename to docs/uptime-guide/images/check-history.png diff --git a/docs/uptime-guide/images/create-alert.png b/docs/uptime-guide/images/create-alert.png new file mode 100644 index 0000000000000..54a0c400cad4c Binary files /dev/null and b/docs/uptime-guide/images/create-alert.png differ diff --git a/docs/uptime/images/crosshair-example.png b/docs/uptime-guide/images/crosshair-example.png similarity index 100% rename from docs/uptime/images/crosshair-example.png rename to docs/uptime-guide/images/crosshair-example.png diff --git a/docs/uptime/images/filter-bar.png b/docs/uptime-guide/images/filter-bar.png similarity index 100% rename from docs/uptime/images/filter-bar.png rename to docs/uptime-guide/images/filter-bar.png diff --git a/docs/uptime-guide/images/indices.png b/docs/uptime-guide/images/indices.png new file mode 100644 index 0000000000000..4090747b6726c Binary files /dev/null and b/docs/uptime-guide/images/indices.png differ diff --git a/docs/uptime/images/monitor-charts.png b/docs/uptime-guide/images/monitor-charts.png similarity index 100% rename from docs/uptime/images/monitor-charts.png rename to docs/uptime-guide/images/monitor-charts.png diff --git a/docs/uptime/images/monitor-list.png b/docs/uptime-guide/images/monitor-list.png similarity index 100% rename from docs/uptime/images/monitor-list.png rename to docs/uptime-guide/images/monitor-list.png diff --git a/docs/uptime-guide/images/monitor-status-alert.png b/docs/uptime-guide/images/monitor-status-alert.png new file mode 100644 index 0000000000000..847a0f58f02ce Binary files /dev/null and b/docs/uptime-guide/images/monitor-status-alert.png differ diff --git a/docs/uptime/images/observability_integrations.png b/docs/uptime-guide/images/observability_integrations.png similarity index 100% rename from docs/uptime/images/observability_integrations.png rename to docs/uptime-guide/images/observability_integrations.png diff --git a/docs/uptime/images/settings.png b/docs/uptime-guide/images/settings.png similarity index 100% rename from docs/uptime/images/settings.png rename to docs/uptime-guide/images/settings.png diff --git a/docs/uptime/images/snapshot-view.png b/docs/uptime-guide/images/snapshot-view.png similarity index 100% rename from docs/uptime/images/snapshot-view.png rename to docs/uptime-guide/images/snapshot-view.png diff --git a/docs/uptime/images/status-bar.png b/docs/uptime-guide/images/status-bar.png similarity index 100% rename from docs/uptime/images/status-bar.png rename to docs/uptime-guide/images/status-bar.png diff --git a/docs/uptime-guide/images/tls-alert.png b/docs/uptime-guide/images/tls-alert.png new file mode 100644 index 0000000000000..19efe07838903 Binary files /dev/null and b/docs/uptime-guide/images/tls-alert.png differ diff --git a/docs/uptime-guide/images/uptime-overview.png b/docs/uptime-guide/images/uptime-overview.png new file mode 100644 index 0000000000000..25c88b2d14287 Binary files /dev/null and b/docs/uptime-guide/images/uptime-overview.png differ diff --git a/docs/uptime-guide/index.asciidoc b/docs/uptime-guide/index.asciidoc index 09763182fa88f..01a93cb454ea9 100644 --- a/docs/uptime-guide/index.asciidoc +++ b/docs/uptime-guide/index.asciidoc @@ -1,5 +1,3 @@ -// short-version can be: 8, 7, 6, etc. -:short-version: 8 include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[] include::{asciidoc-dir}/../../shared/attributes.asciidoc[] @@ -12,3 +10,13 @@ include::install.asciidoc[] include::deployment-arch.asciidoc[] +include::app-overview.asciidoc[] + +include::monitor.asciidoc[] + +include::settings.asciidoc[] + +include::certificates.asciidoc[] + +include::alerting.asciidoc[] + diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc index 0ed1270ca92ce..05b9c6665562f 100644 --- a/docs/uptime-guide/install.asciidoc +++ b/docs/uptime-guide/install.asciidoc @@ -29,7 +29,7 @@ first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for [[install-elasticsearch]] === Step 1: Install Elasticsearch -Install an Elasticsearch cluster, start it up, and make sure it's running. +Install an {es} cluster, start it up, and make sure it's running. . Verify that your system meets the https://www.elastic.co/support/matrix#matrix_jvm[minimum JVM requirements] for {es}. @@ -39,7 +39,7 @@ https://www.elastic.co/support/matrix#matrix_jvm[minimum JVM requirements] for { [[install-kibana]] === Step 2: Install Kibana -Install Kibana, start it up, and open up the web interface: +Install {kib}, start it up, and open up the web interface: . {stack-gs}/get-started-elastic-stack.html#install-kibana[Install Kibana]. . {stack-gs}/get-started-elastic-stack.html#_launch_the_kibana_web_interface[Launch the Kibana Web Interface]. @@ -48,27 +48,27 @@ Install Kibana, start it up, and open up the web interface: === Step 3: Install and configure Heartbeat Uptime requires the setup of monitors in Heartbeat. -These monitors provide the data you'll be visualizing in the {kibana-ref}/xpack-uptime.html[Uptime UI]. +These monitors provide the data you'll be visualizing in the {kibana-ref}/xpack-uptime.html[Uptime app]. -See the *Setup Instructions* in Kibana for instructions on installing and configuring Heartbeat. +For instructions on installing and configuring Heartbeat, see the *Setup Instructions* in {kib}. Additional information is available in {heartbeat-ref}/heartbeat-configuration.html[Configure Heartbeat]. [role="screenshot"] image::images/uptime-setup.png[Installation instructions on the Uptime page in Kibana] [[setup-security]] -=== Step 4: Setup Security +=== Step 4: Set up Security Secure your installation by following the {heartbeat-ref}/securing-heartbeat.html[Secure Heartbeat] documentation. [float] ==== Important considerations -* Make sure you're using the same major versions of Heartbeat and Kibana. +* Make sure you're using the same major versions of Heartbeat and {kib}. -* Index patterns tell Kibana which Elasticsearch indices you want to explore. -The Uptime UI requires a +heartbeat-{short-version}*+ index pattern. -If you have configured a different index pattern, you can use {ref}/indices-aliases.html[index aliases] to ensure data is recognized by the UI. +* Index patterns tell {kib} which {es} indices you want to explore. +The Uptime app requires a +heartbeat-{major-version-only}*+ index pattern. +If you have configured a different index pattern, you can use {ref}/indices-aliases.html[index aliases] to ensure data is recognized by the Uptime app. After you install and configure Heartbeat, -the {kibana-ref}/xpack-uptime.html[Uptime UI] will automatically populate with the Heartbeat monitors. +the {kibana-ref}/xpack-uptime.html[Uptime app] is automatically populated with the Heartbeat monitors. diff --git a/docs/uptime-guide/monitor.asciidoc b/docs/uptime-guide/monitor.asciidoc new file mode 100644 index 0000000000000..bb5d315cf63eb --- /dev/null +++ b/docs/uptime-guide/monitor.asciidoc @@ -0,0 +1,59 @@ +[role="xpack"] +[[uptime-monitor]] +=== Monitor + +The Monitor page helps you gain insights into the performance +of a specific network endpoint. A detailed visualization of +the monitor's request duration over time, as well as the `up`/`down` +status over time, is displayed. By configuring Machine Learning jobs +on this page, you can also also detect anomalies in response time data. + + +==== Status panel + +The Status panel displays a quick summary of the latest information +regarding your monitor. You can view its latest status, click a link to +visit the targeted URL, see its most recent request duration, and determine the +amount of time that has elapsed since the last check. + +When two Heartbeat instances are configured in different geographic locations +the map will show each location as a pinpoint on the map, along with the +amount of time elapsed since data was last received from that location. + +[role="screenshot"] +image::images/status-bar.png[Status bar] + + +[float] +==== Monitor charts + +The Monitor charts visualize information over the time specified in the +date range. These charts help you gain insights into how quickly requests are being resolved +by the targeted endpoint, and give you a sense of how frequently a host or endpoint +was down in your selected timespan. + +[role="screenshot"] +image::images/monitor-charts.png[Monitor charts] + +The Monitor duration chart displays request duration information for your monitor. +The area surrounding the line is the range of request time for the corresponding +bucket. The line is the average time. In the upper right hand of this panel +you can enable Anomaly detection using Machine Learning. When response times change +in an unexpected way the time range in which they occurred are highlighted with a color. + +The pings over time chart is a graphical representation of the check statuses over time. +Hover over the charts to display crosshairs with specific numeric data. + +[role="screenshot"] +image::images/crosshair-example.png[Chart crosshair] + +[float] +==== Check history + +The Check history table lists the total count of this monitor's checks for the selected +date range. To help find recent problems on a per-check basis, you can filter the checks +by status and location. This table can help you gain some insight into more granular details +about recent individual data points that Heartbeat is logging about your host or endpoint. + +[role="screenshot"] +image::images/check-history.png[Check history view] diff --git a/docs/uptime-guide/overview.asciidoc b/docs/uptime-guide/overview.asciidoc index c6bd71b1f5574..ab230b27f8cda 100644 --- a/docs/uptime-guide/overview.asciidoc +++ b/docs/uptime-guide/overview.asciidoc @@ -2,10 +2,14 @@ [[uptime-overview]] == Elastic Uptime overview -Elastic Uptime allows you to monitor the availability and response times of applications and services in real time and to detect problems before they affect users. +++++ +Overview +++++ -Elastic Uptime can help you to understand uptime and response time characteristics for your services and applications. -It can be deployed both inside and outside your organization's network, so you can analyze problems from multiple vantage points. +Elastic Uptime enables you to monitor the availability and response times of applications and services in real time and to detect problems before they affect users. + +Elastic Uptime helps you to understand uptime and response time characteristics for your services and applications. +It can be deployed both inside and outside your organization's network, so that you can analyze problems from multiple vantage points. Elastic Uptime uses these components: *Heartbeat*, *Elasticsearch* and *Kibana*. @@ -37,13 +41,11 @@ The {kibana-ref}/xpack-uptime.html[Elasticsearch Uptime app] in Kibana provides // ++ In diagram, should be Uptime app, not Uptime UI, possibly even Elastic Uptime? Also applies to Metrics/Logging/APM. // ++ Need more whitespace around components. -image::images/uptime-simple-deployment.png[Uptime simple deployment] - In this simple deployment, a single instance of Heartbeat is deployed at a single monitoring location to monitor a single service. The Heartbeat instance sends the monitoring data to Elasticsearch. Then you can use the Uptime app in Kibana to view the data from Heartbeat and determine the status of the service. -image::images/uptime-multi-deployment.png[Uptime multiple server deployment] +image::images/uptime-simple-deployment.png[Uptime simple deployment] In this deployment, two instances of Heartbeat are deployed at two different monitoring locations. Both instances monitor the same service. @@ -51,3 +53,5 @@ The Heartbeat instances send the monitoring data to Elasticsearch. As before, you can use the Uptime app in Kibana to view the Heartbeat data and determine the status of the service. When a failure occurs, the multiple monitoring locations enable you to pinpoint the area in which the failure has occurred. +image::images/uptime-multi-deployment.png[Uptime multiple server deployment] + diff --git a/docs/uptime-guide/settings.asciidoc b/docs/uptime-guide/settings.asciidoc new file mode 100644 index 0000000000000..59f9af631bfa7 --- /dev/null +++ b/docs/uptime-guide/settings.asciidoc @@ -0,0 +1,51 @@ +[role="xpack"] +[[uptime-settings]] + +=== Settings + +The Uptime settings page lets you change which Heartbeat indices are displayed +by the uptime app. Users must have the 'all' permission to modify items on this page. +Uptime settings apply to the current space only. Use different settings in different +spaces to segment different uptime use cases and domains. + +==== Indices + +Imagine your organization has one team for internal IT services, and another +for public services. Each team operates independently and is only responsible for its +own services. In this scenario, you might set up separate Heartbeat instances for each team, +writing out to index patterns named `it-heartbeat-\*`, and `external-heartbeat-\*`. You would +create separate roles and users for each in Elasticsearch, each with access to their own spaces, +named `it` and `external` respectively. Within each space you would navigate to the settings page +and set the correct index pattern to match only the indices that space is allowed to access. + +Note: The pattern set here only restricts what the Uptime app shows. Users may still be able +to manually query Elasticsearch for data outside this pattern. + +[role="screenshot"] +image::images/indices.png[Heartbeat indices] + +See the {kibana-ref}/uptime-security.html[Uptime security] and {heartbeat-ref}/securing-heartbeat.html[Heartbeat security] +docs for more information. + +==== Certificate thresholds + +You can modify settings in this section to control how Uptime will visualize your TLS values in +the <>. These settings also determine which certificates will be +selected by any TLS alert you define. + +There are two fields, `age` and `expiration`. Use the `age` threshold to specify when Uptime should warn +you about certificates that have been valid for too long. Use the `expiration` threshold to specify when Uptime should warn you +about certificates that have approaching expiration dates. + +For example, a common security requirement is to make sure that none of your organization's TLS certificates have been +valid for longer than one year. Modifying the `Age limit` field's value to 365 days will help you keep track of which +certificates you may want to refresh. + +Likewise, to see which of your TLS certificates are close to expiring ahead of time, specify +an `Expiration threshold` on this page. When the count of a certificate's remaining valid days falls +below this threshold, Uptime will consider it in a warning state. When you define a TLS alert, you receive a +notification from Uptime about the certificate. + +[role="screenshot"] +image::images/cert-exp.png[Certification expiration thresholds] + diff --git a/docs/uptime/alerting.asciidoc b/docs/uptime/alerting.asciidoc deleted file mode 100644 index 24f7628e960f9..0000000000000 --- a/docs/uptime/alerting.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[role="xpack"] -[[uptime-alerting]] - -== Uptime alerting - -The Uptime app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] -feature. It provides a set of built-in actions and Uptime specific threshold alerts for you to use -and enables central management of all alerts from <>. - -[float] -=== Monitor status alerts - -To receive alerts when a monitor goes down, use the alerting menu at the top of the -overview page. Use a query in the alert flyout to determine which monitors to check -with your alert. If you already have a query in the overview page search bar it will -be carried over into this box. - -[role="screenshot"] -image::uptime/images/monitor-status-alert-flyout.png[Create monitor status alert flyout] - -[float] -=== TLS alerts - -Uptime also provides the ability to create an alert that will notify you when one or -more of your monitors have a TLS certificate that will expire within some threshold, -or when its age exceeds a limit. The values for these thresholds are configurable on -the <>. - -[role="screenshot"] -image::uptime/images/tls-alert-flyout.png[Create TLS alert flyout] diff --git a/docs/uptime/certificates.asciidoc b/docs/uptime/certificates.asciidoc deleted file mode 100644 index cc604d7196648..0000000000000 --- a/docs/uptime/certificates.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[role="xpack"] -[[uptime-certificates]] - -== Certificates - -[role="screenshot"] -image::uptime/images/certificates-page.png[Certificates] - -The certificates page allows you to visualize TLS certificate data in your indices. In addition to the -common name, associated monitors, issuer information, and SHA fingerprints, Uptime also assigns a status -derived from the threshold values in the <>. - -Several of the columns on this page are sortable. You can use the search bar at the top of the view -to find values in most of the TLS-related fields in your Uptime indices. Additionally, you can -create a TLS alert using the `Alerts` dropdown at the top of the page. diff --git a/docs/uptime/images/monitor-status-alert-flyout.png b/docs/uptime/images/monitor-status-alert-flyout.png deleted file mode 100644 index 407e69fc5e86e..0000000000000 Binary files a/docs/uptime/images/monitor-status-alert-flyout.png and /dev/null differ diff --git a/docs/uptime/images/tls-alert-flyout.png b/docs/uptime/images/tls-alert-flyout.png deleted file mode 100644 index 07c725c858a00..0000000000000 Binary files a/docs/uptime/images/tls-alert-flyout.png and /dev/null differ diff --git a/docs/uptime/images/uptime-overview.png b/docs/uptime/images/uptime-overview.png new file mode 100644 index 0000000000000..25c88b2d14287 Binary files /dev/null and b/docs/uptime/images/uptime-overview.png differ diff --git a/docs/uptime/index.asciidoc b/docs/uptime/index.asciidoc index c44ef366eaaa4..66c9e9357420f 100644 --- a/docs/uptime/index.asciidoc +++ b/docs/uptime/index.asciidoc @@ -1,25 +1,19 @@ +[chapter] [role="xpack"] [[xpack-uptime]] = Uptime -[partintro] --- -Uptime allows you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. +The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. You can explore endpoint status over time, drill down into specific monitors, -and easily view a high-level snapshot of your environment at any point in time. +and view a high-level snapshot of your environment at any point in time. + +[role="screenshot"] +image::images/uptime-overview.png[Uptime app overview] + +[float] +=== Get started To get started with Elastic Uptime, refer to {uptime-guide}/install-uptime.html[Install Uptime]. -* <> -* <> -* <> -* <> -* <> --- -include::overview.asciidoc[] -include::monitor.asciidoc[] -include::settings.asciidoc[] -include::certificates.asciidoc[] -include::alerting.asciidoc[] diff --git a/docs/uptime/monitor.asciidoc b/docs/uptime/monitor.asciidoc deleted file mode 100644 index 8a4be1f11a721..0000000000000 --- a/docs/uptime/monitor.asciidoc +++ /dev/null @@ -1,59 +0,0 @@ -[role="xpack"] -[[uptime-monitor]] -== Monitor - -The Monitor page will help you get further insight into the performance -of a specific network endpoint. You'll see a detailed visualization of -the monitor's request duration over time, as well as the `up`/`down` -status over time. You can also also detect anomalies in response time data -by configuring Machine Learning jobs on this page. - -[float] -=== Status panel - -[role="screenshot"] -image::uptime/images/status-bar.png[Status bar] - -The Status panel displays a quick summary of the latest information -regarding your monitor. You can view its latest status, click a link to -visit the targeted URL, see its most recent request duration, and determine the -amount of time that has elapsed since the last check. - -When two Heartbeat instances are configured in different geographic locations -the map will show each location as a pinpoint on the map, along with the -amount of time elapsed since data was last received from that location. - - -[float] -=== Monitor charts - -[role="screenshot"] -image::uptime/images/monitor-charts.png[Monitor charts] - -The Monitor charts visualize information over the time specified in the -date range. These charts can help you gain insight into how quickly requests are being resolved -by the targeted endpoint, and give you a sense of how frequently a host or endpoint -was down in your selected timespan. - -The Monitor duration chart displays request duration information for your monitor. -The area surrounding the line is the range of request time for the corresponding -bucket. The line is the average time. Anomaly detection using Machine Learning -can be configured in the upper right hand of this panel. When response times change -in an unexpected way the time range in which they occurred will be given filled with a color. - -The pings over time chart is a graphical representation of the check statuses over time. -Hover over the charts to display crosshairs with more specific numeric data. - -[role="screenshot"] -image::uptime/images/crosshair-example.png[Chart crosshair] - -[float] -=== Check history - -[role="screenshot"] -image::uptime/images/check-history.png[Check history view] - -The Check history displays the total count of this monitor's checks for the selected -date range. You can additionally filter the checks by status and location to help find recent problems -on a per-check basis. This table can help you gain some insight into more granular details -about recent individual data points Heartbeat is logging about your host or endpoint. diff --git a/docs/uptime/overview.asciidoc b/docs/uptime/overview.asciidoc deleted file mode 100644 index b449beddd240c..0000000000000 --- a/docs/uptime/overview.asciidoc +++ /dev/null @@ -1,62 +0,0 @@ -[role="xpack"] -[[uptime-overview]] - -== Overview - -The Uptime overview is intended to help you quickly identify and diagnose outages and -other connectivity issues within your network or environment. There is a date range -selection that is global to the Uptime UI; you can use this selection to highlight -an absolute date range, or a relative one, similar to other areas of Kibana. - -[float] -=== Filter bar - -[role="screenshot"] -image::uptime/images/filter-bar.png[Filter bar] - -The filter bar is designed to let you quickly view specific groups of monitors, or even -an individual monitor, if you have defined many. - -This control allows you to use automated filter options, as well as input custom filter -text to select specific monitors by field, URL, ID, and other attributes. - -[float] -=== Snapshot panel - -[role="screenshot"] -image::uptime/images/snapshot-view.png[Snapshot view] - -This panel is intended to quickly give you a sense of the overall -status of the environment you're monitoring, or a subset of those monitors. -Here, you can see the total number of detected monitors within the selected -Uptime date range. In addition to the total, the counts for the number of monitors -in an `up` or `down` state are displayed, based on the last check reported by Heartbeat -for each monitor. - -Next to the counts, there is a histogram displaying the change over time throughout the -selected date range. - -[float] -=== Monitor list - -[role="screenshot"] -image::uptime/images/monitor-list.png[Monitor list] - -The Monitor list displays information at the level of individual monitors. -The data shown here will flesh out your individual monitors, and provide a quick -way to navigate to a more in-depth visualization for interesting hosts or endpoints. - -This table includes information like the most recent status, when the monitor was last checked, its -ID and URL, its IP address, and a dedicated sparkline showing its check status over time. - -[float] -=== Observability integrations - -[role="screenshot"] -image::uptime/images/observability_integrations.png[Observability integrations] - -The Monitor list also contains a menu of possible integrations. If Uptime detects Kubernetes or -Docker related host information, it will provide links to open the Metrics app or Logs app pre-filtered -for this host. Additionally, this feature supplies links to simply filter the other views on the host's -IP address, to help you quickly determine if these other solutions contain data relevant to your current -interest. diff --git a/docs/uptime/settings.asciidoc b/docs/uptime/settings.asciidoc deleted file mode 100644 index 131772609cb59..0000000000000 --- a/docs/uptime/settings.asciidoc +++ /dev/null @@ -1,48 +0,0 @@ -[role="xpack"] -[[uptime-settings]] - -== Settings - -[role="screenshot"] -image::uptime/images/settings.png[Settings page] - -=== Indices - -The Uptime settings page lets you change which Heartbeat indices are displayed -by the uptime app. Users must have the 'all' permission to modify items on this page. -Uptime settings apply to the current space only. Use different settings in different -spaces to segment different uptime use cases and domains. - -As an example, imagine your organization has one team for internal IT services, and another -for public services. Each team operates independently and is only responsible for its -own services. In this scenario, you might set up separate Heartbeat instances for each team, -writing out to index patterns named `it-heartbeat-\*`, and `external-heartbeat-\*`. You would -create separate roles and users for each in Elasticsearch, each with access to their own spaces, -named `it` and `external` respectively. Within each space you would navigate to the settings page -and set the correct index pattern to match only the indices that space is allowed to access. - -Note that the pattern set here only restricts what the Uptime app shows. Users may still be able -to manually query Elasticsearch for data outside this pattern! - -See the <> -and {heartbeat-ref}/securing-heartbeat.html[Heartbeat security] -docs for more information. - -=== Certificate thresholds - -You can modify settings in this section to control how Uptime will visualize your TLS values in the Certificates page. -These settings also determine which certificates will be selected by any TLS alert you define. - -There are two fields, `age` and `expiration`. Use the `age` threshold to specify when Uptime should warn -you about certificates that have been valid for too long. Use the `expiration` threshold to make Uptime warn you -about certificates that have approaching expiration dates. - -For example, a common security requirement is to make sure that none of your organization's TLS certificates have been -valid for longer than one year. Modifying the `Age limit` field's value to 365 days will help you keep track of which -certificates you may want to refresh. - -Likewise, to see which of your TLS certificates are close to expiring ahead of time, specify -an `Expiration threshold` on this page. When the count of a certificate's remaining valid days falls -below this threshold, Uptime will consider it in a warning state. If you have defined a TLS alert, you will -receive a notification from Uptime about the certificate. - diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 98033c5a87f6f..355684f7448a1 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -5,7 +5,7 @@ [partintro] -- -Canvas is a data visualization and presentation tool that sits within Kibana. With Canvas, you can pull live data directly from Elasticsearch, and combine the data with colors, images, text, and your imagination to create dynamic, multi-page, pixel-perfect displays. If you are a little bit creative, a little bit technical, and a whole lot curious, then Canvas is for you. +Canvas is a data visualization and presentation tool that sits within {kib}. With Canvas, you can pull live data directly from {es}, and combine the data with colors, images, text, and your imagination to create dynamic, multi-page, pixel-perfect displays. If you are a little bit creative, a little bit technical, and a whole lot curious, then Canvas is for you. With Canvas, you can: @@ -13,9 +13,7 @@ With Canvas, you can: * Customize your workpad with your own visualizations, such as images and text. -* Customize your data by pulling it directly from Elasticsearch. - -* Show off your data with charts, graphs, progress monitors, and more. +* Pull your data directly from Elasticsearch, then show it off with charts, graphs, progress monitors, and more. * Focus the data you want to display with filters. diff --git a/package.json b/package.json index cc1f7eb6c1dd3..0873ab8c158e1 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "@elastic/eui": "23.3.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", - "@elastic/numeral": "2.4.0", + "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", "@elastic/ui-ace": "0.2.3", "@hapi/good-squeeze": "5.2.1", @@ -365,7 +365,6 @@ "@types/node": ">=10.17.17 <10.20.0", "@types/node-forge": "^0.9.0", "@types/normalize-path": "^3.0.0", - "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", "@types/pngjs": "^3.3.2", @@ -430,7 +429,7 @@ "eslint-plugin-prefer-object-spread": "^1.2.1", "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-react": "^7.17.0", - "eslint-plugin-react-hooks": "^2.3.0", + "eslint-plugin-react-hooks": "^4.0.4", "eslint-plugin-react-perf": "^3.2.3", "exit-hook": "^2.2.0", "faker": "1.1.0", @@ -505,7 +504,7 @@ "zlib": "^1.0.5" }, "engines": { - "node": "10.19.0", + "node": "10.21.0", "yarn": "^1.21.1" } } diff --git a/packages/eslint-config-kibana/.eslintrc.js b/packages/eslint-config-kibana/.eslintrc.js index 624ee4679a3b9..c0f8bf0ecb508 100644 --- a/packages/eslint-config-kibana/.eslintrc.js +++ b/packages/eslint-config-kibana/.eslintrc.js @@ -39,6 +39,10 @@ module.exports = { to: false, disallowedMessage: `Don't use 'mkdirp', use the new { recursive: true } option of Fs.mkdir instead` }, + { + from: 'numeral', + to: '@elastic/numeral', + }, { from: '@kbn/elastic-idx', to: false, @@ -52,6 +56,15 @@ module.exports = { from: 'react-router', to: 'react-router-dom', }, + { + from: '@kbn/ui-shared-deps/monaco', + to: '@kbn/monaco', + }, + { + from: 'monaco-editor', + to: false, + disallowedMessage: `Don't import monaco directly, use or add exports to @kbn/monaco` + }, ], ], }, diff --git a/packages/kbn-config-schema/package.json b/packages/kbn-config-schema/package.json index 71c0ae4bff1f9..06342127b0d89 100644 --- a/packages/kbn-config-schema/package.json +++ b/packages/kbn-config-schema/package.json @@ -10,7 +10,8 @@ "kbn:bootstrap": "yarn build" }, "devDependencies": { - "typescript": "3.7.2" + "typescript": "3.7.2", + "tsd": "^0.7.4" }, "peerDependencies": { "joi": "^13.5.2", diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index 5d387f327e58f..2319fe4395e3f 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -44,6 +44,7 @@ import { ObjectType, ObjectTypeOptions, Props, + NullableProps, RecordOfOptions, RecordOfType, StringOptions, @@ -57,7 +58,7 @@ import { StreamType, } from './types'; -export { ObjectType, TypeOf, Type }; +export { ObjectType, TypeOf, Type, Props, NullableProps }; export { ByteSizeValue } from './byte_size_value'; export { SchemaTypeError, ValidationError } from './errors'; export { isConfigSchema } from './typeguards'; diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index 9db79b8bf9e00..c7900e1923e78 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -29,7 +29,7 @@ export { LiteralType } from './literal_type'; export { MaybeType } from './maybe_type'; export { MapOfOptions, MapOfType } from './map_type'; export { NumberOptions, NumberType } from './number_type'; -export { ObjectType, ObjectTypeOptions, Props, TypeOf } from './object_type'; +export { ObjectType, ObjectTypeOptions, Props, NullableProps, TypeOf } from './object_type'; export { RecordOfOptions, RecordOfType } from './record_type'; export { StreamType } from './stream_type'; export { StringOptions, StringType } from './string_type'; diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 5ab59d1c02077..334e814aa52e4 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { expectType } from 'tsd'; import { schema } from '..'; import { TypeOf } from './object_type'; @@ -360,17 +361,142 @@ test('handles optional properties', () => { type SchemaType = TypeOf; - let foo: SchemaType = { + expectType({ required: 'foo', - }; - foo = { + }); + expectType({ required: 'hello', optional: undefined, - }; - foo = { + }); + expectType({ required: 'hello', optional: 'bar', - }; + }); +}); + +describe('#extends', () => { + it('allows to extend an existing schema by adding new properties', () => { + const origin = schema.object({ + initial: schema.string(), + }); + + const extended = origin.extends({ + added: schema.number(), + }); + + expect(() => { + extended.validate({ initial: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"[added]: expected value of type [number] but got [undefined]"` + ); + + expect(() => { + extended.validate({ initial: 'foo', added: 42 }); + }).not.toThrowError(); - expect(foo).toBeDefined(); + expectType>({ + added: 12, + initial: 'foo', + }); + }); + + it('allows to extend an existing schema by removing properties', () => { + const origin = schema.object({ + string: schema.string(), + number: schema.number(), + }); + + const extended = origin.extends({ number: undefined }); + + expect(() => { + extended.validate({ string: 'foo', number: 12 }); + }).toThrowErrorMatchingInlineSnapshot(`"[number]: definition for this key is missing"`); + + expect(() => { + extended.validate({ string: 'foo' }); + }).not.toThrowError(); + + expectType>({ + string: 'foo', + }); + }); + + it('allows to extend an existing schema by overriding an existing properties', () => { + const origin = schema.object({ + string: schema.string(), + mutated: schema.number(), + }); + + const extended = origin.extends({ + mutated: schema.string(), + }); + + expect(() => { + extended.validate({ string: 'foo', mutated: 12 }); + }).toThrowErrorMatchingInlineSnapshot( + `"[mutated]: expected value of type [string] but got [number]"` + ); + + expect(() => { + extended.validate({ string: 'foo', mutated: 'bar' }); + }).not.toThrowError(); + + expectType>({ + string: 'foo', + mutated: 'bar', + }); + }); + + it('properly infer the type from optional properties', () => { + const origin = schema.object({ + original: schema.maybe(schema.string()), + mutated: schema.maybe(schema.number()), + removed: schema.maybe(schema.string()), + }); + + const extended = origin.extends({ + removed: undefined, + mutated: schema.string(), + }); + + expect(() => { + extended.validate({ original: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"[mutated]: expected value of type [string] but got [undefined]"` + ); + expect(() => { + extended.validate({ original: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"[mutated]: expected value of type [string] but got [undefined]"` + ); + expect(() => { + extended.validate({ original: 'foo', mutated: 'bar' }); + }).not.toThrowError(); + + expectType>({ + original: 'foo', + mutated: 'bar', + }); + expectType>({ + mutated: 'bar', + }); + }); + + it(`allows to override the original schema's options`, () => { + const origin = schema.object( + { + initial: schema.string(), + }, + { defaultValue: { initial: 'foo' } } + ); + + const extended = origin.extends( + { + added: schema.number(), + }, + { defaultValue: { initial: 'bar', added: 42 } } + ); + + expect(extended.validate(undefined)).toEqual({ initial: 'bar', added: 42 }); + }); }); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index fee2d02c1bfb9..431b6e905bcd4 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -24,6 +24,8 @@ import { ValidationError } from '../errors'; export type Props = Record>; +export type NullableProps = Record | undefined | null>; + export type TypeOf> = RT['type']; type OptionalProperties = Pick< @@ -47,6 +49,24 @@ export type ObjectResultType

= Readonly< { [K in keyof RequiredProperties

]: TypeOf } >; +type DefinedProperties = Pick< + Base, + { + [Key in keyof Base]: undefined extends Base[Key] ? never : null extends Base[Key] ? never : Key; + }[keyof Base] +>; + +type ExtendedProps

= Omit & + { [K in keyof DefinedProperties]: NP[K] }; + +type ExtendedObjectType

= ObjectType< + ExtendedProps +>; + +type ExtendedObjectTypeOptions

= ObjectTypeOptions< + ExtendedProps +>; + interface UnknownOptions { /** * Options for dealing with unknown keys: @@ -61,10 +81,13 @@ export type ObjectTypeOptions

= TypeOptions extends Type> { - private props: Record; + private props: P; + private options: ObjectTypeOptions

; + private propSchemas: Record; - constructor(props: P, { unknowns = 'forbid', ...typeOptions }: ObjectTypeOptions

= {}) { + constructor(props: P, options: ObjectTypeOptions

= {}) { const schemaKeys = {} as Record; + const { unknowns = 'forbid', ...typeOptions } = options; for (const [key, value] of Object.entries(props)) { schemaKeys[key] = value.getSchema(); } @@ -77,7 +100,93 @@ export class ObjectType

extends Type> .options({ stripUnknown: { objects: unknowns === 'ignore' } }); super(schema, typeOptions); - this.props = schemaKeys; + this.props = props; + this.propSchemas = schemaKeys; + this.options = options; + } + + /** + * Return a new `ObjectType` instance extended with given `newProps` properties. + * Original properties can be deleted from the copy by passing a `null` or `undefined` value for the key. + * + * @example + * How to add a new key to an object schema + * ```ts + * const origin = schema.object({ + * initial: schema.string(), + * }); + * + * const extended = origin.extends({ + * added: schema.number(), + * }); + * ``` + * + * How to remove an existing key from an object schema + * ```ts + * const origin = schema.object({ + * initial: schema.string(), + * toRemove: schema.number(), + * }); + * + * const extended = origin.extends({ + * toRemove: undefined, + * }); + * ``` + * + * How to override the schema's options + * ```ts + * const origin = schema.object({ + * initial: schema.string(), + * }, { defaultValue: { initial: 'foo' }}); + * + * const extended = origin.extends({ + * added: schema.number(), + * }, { defaultValue: { initial: 'foo', added: 'bar' }}); + * + * @remarks + * `extends` only support extending first-level properties. It's currently not possible to perform deep/nested extensions. + * + * ```ts + * const origin = schema.object({ + * foo: schema.string(), + * nested: schema.object({ + * a: schema.string(), + * b: schema.string(), + * }), + * }); + * + * const extended = origin.extends({ + * nested: schema.object({ + * c: schema.string(), + * }), + * }); + * + * // TypeOf is `{ foo: string; nested: { c: string } }` + * ``` + */ + public extends( + newProps: NP, + newOptions?: ExtendedObjectTypeOptions + ): ExtendedObjectType { + const extendedProps = Object.entries({ + ...this.props, + ...newProps, + }).reduce((memo, [key, value]) => { + if (value !== null && value !== undefined) { + return { + ...memo, + [key]: value, + }; + } + return memo; + }, {} as ExtendedProps); + + const extendedOptions = { + ...this.options, + ...newOptions, + } as ExtendedObjectTypeOptions; + + return new ObjectType(extendedProps, extendedOptions); } protected handleError(type: string, { reason, value }: Record) { @@ -95,10 +204,10 @@ export class ObjectType

extends Type> } validateKey(key: string, value: any) { - if (!this.props[key]) { + if (!this.propSchemas[key]) { throw new Error(`${key} is not a valid part of this schema`); } - const { value: validatedValue, error } = this.props[key].validate(value); + const { value: validatedValue, error } = this.propSchemas[key].validate(value); if (error) { throw new ValidationError(error as any, key); } diff --git a/packages/kbn-monaco/README.md b/packages/kbn-monaco/README.md new file mode 100644 index 0000000000000..c5d7dd7dbfed5 --- /dev/null +++ b/packages/kbn-monaco/README.md @@ -0,0 +1,5 @@ +# @kbn/monaco + +A customized version of monaco that is automatically configured the way we want it to be when imported as `@kbn/monaco`. Additionally, imports to this package are automatically shared with all plugins using `@kbn/ui-shared-deps`. + +Includes custom xjson language support. The `es_ui_shared` plugin has an example of how to use it, in the future we will likely expose helpers from this package to make using it everywhere a little more seamless. \ No newline at end of file diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json new file mode 100644 index 0000000000000..170c014e6e326 --- /dev/null +++ b/packages/kbn-monaco/package.json @@ -0,0 +1,27 @@ +{ + "name": "@kbn/monaco", + "version": "1.0.0", + "private": true, + "main": "./target/index.js", + "license": "Apache-2.0", + "scripts": { + "build": "node ./scripts/build.js", + "kbn:bootstrap": "yarn build --dev" + }, + "dependencies": { + "regenerator-runtime": "^0.13.3", + "monaco-editor": "~0.17.0" + }, + "devDependencies": { + "@kbn/babel-preset": "1.0.0", + "@kbn/dev-utils": "1.0.0", + "babel-loader": "^8.0.6", + "css-loader": "^3.4.2", + "del": "^5.1.0", + "raw-loader": "3.1.0", + "supports-color": "^7.0.0", + "typescript": "3.7.2", + "webpack": "^4.41.5", + "webpack-cli": "^3.3.10" + } +} diff --git a/packages/kbn-monaco/scripts/build.js b/packages/kbn-monaco/scripts/build.js new file mode 100644 index 0000000000000..c5540e3c956c8 --- /dev/null +++ b/packages/kbn-monaco/scripts/build.js @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const path = require('path'); +const del = require('del'); +const supportsColor = require('supports-color'); +const { run } = require('@kbn/dev-utils'); + +const TARGET_BUILD_DIR = path.resolve(__dirname, '../target'); +const ROOT_DIR = path.resolve(__dirname, '../'); +const WEBPACK_CONFIG_PATH = path.resolve(ROOT_DIR, 'webpack.config.js'); + +run( + async ({ procRunner, log, flags }) => { + log.info('Deleting old output'); + + await del(TARGET_BUILD_DIR); + + const cwd = ROOT_DIR; + const env = { ...process.env }; + if (supportsColor.stdout) { + env.FORCE_COLOR = 'true'; + } + + await procRunner.run('worker', { + cmd: 'webpack', + args: ['--config', WEBPACK_CONFIG_PATH, flags.dev ? '--env.dev' : '--env.prod'], + wait: true, + env, + cwd, + }); + + await procRunner.run('tsc ', { + cmd: 'tsc', + args: [], + wait: true, + env, + cwd, + }); + + log.success('Complete'); + }, + { + flags: { + boolean: ['dev'], + }, + } +); diff --git a/packages/kbn-monaco/src/index.ts b/packages/kbn-monaco/src/index.ts new file mode 100644 index 0000000000000..9213a1bfe1327 --- /dev/null +++ b/packages/kbn-monaco/src/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { monaco } from './monaco'; +export { XJsonLang } from './xjson'; + +/* eslint-disable-next-line @kbn/eslint/module_migration */ +import * as BarePluginApi from 'monaco-editor/esm/vs/editor/editor.api'; +export { BarePluginApi }; diff --git a/packages/kbn-ui-shared-deps/monaco.ts b/packages/kbn-monaco/src/monaco.ts similarity index 96% rename from packages/kbn-ui-shared-deps/monaco.ts rename to packages/kbn-monaco/src/monaco.ts index 42801c69a3e2c..a40b2212ef0e2 100644 --- a/packages/kbn-ui-shared-deps/monaco.ts +++ b/packages/kbn-monaco/src/monaco.ts @@ -17,6 +17,8 @@ * under the License. */ +/* eslint-disable @kbn/eslint/module_migration */ + import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import 'monaco-editor/esm/vs/base/common/worker/simpleWorker'; diff --git a/packages/kbn-monaco/src/xjson/README.md b/packages/kbn-monaco/src/xjson/README.md new file mode 100644 index 0000000000000..8652d0bd776d2 --- /dev/null +++ b/packages/kbn-monaco/src/xjson/README.md @@ -0,0 +1,28 @@ +# README + +This folder contains the language definitions for XJSON used by the Monaco editor. + +## Summary of contents + +Note: All source code. + +### ./worker + +The worker proxy and worker instantiation code used in both the main thread and the worker thread. + +### ./lexer_rules + +Contains the Monarch-specific language tokenization rules for XJSON. Each set of rules registers itself against monaco. + +### ./constants.ts + +Contains the unique language ID. + +### ./language + +Takes care of global setup steps for the language (like registering it against Monaco) and exports a way to load up +the grammar parser. + +### ./worker_proxy_service + +A stateful mechanism for holding a reference to the Monaco-provided proxy getter. diff --git a/packages/kbn-monaco/src/xjson/constants.ts b/packages/kbn-monaco/src/xjson/constants.ts new file mode 100644 index 0000000000000..dc107abff4ffe --- /dev/null +++ b/packages/kbn-monaco/src/xjson/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ID = 'xjson'; diff --git a/packages/kbn-monaco/src/xjson/grammar.ts b/packages/kbn-monaco/src/xjson/grammar.ts new file mode 100644 index 0000000000000..e95059f9ece2d --- /dev/null +++ b/packages/kbn-monaco/src/xjson/grammar.ts @@ -0,0 +1,213 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum AnnoTypes { + error = 'error', + warning = 'warning', +} + +/* eslint-disable */ + +export const createParser = () => { + 'use strict'; + let at: any, + annos: any[], // annotations + ch: any, + text: any, + value: any, + escapee: any = { + '"': '"', + '\\': '\\', + '/': '/', + b: '\b', + f: '\f', + n: '\n', + r: '\r', + t: ' ', + }, + error = function (m: string) { + throw { + at: at, + text: m, + message: m, + }; + }, + warning = function (m: string, idx: number) { + annos.push({ + type: AnnoTypes.warning, + at: idx, + text: m, + }); + }, + reset = function (newAt: number) { + ch = text.charAt(newAt); + at = newAt + 1; + }, + next = function (c?: string) { + return ( + c && c !== ch && error("Expected '" + c + "' instead of '" + ch + "'"), + (ch = text.charAt(at)), + (at += 1), + ch + ); + }, + nextUpTo = function (upTo: any, errorMessage: string) { + let currentAt = at, + i = text.indexOf(upTo, currentAt); + if (i < 0) { + error(errorMessage || "Expected '" + upTo + "'"); + } + reset(i + upTo.length); + return text.substring(currentAt, i); + }, + peek = function (c: string) { + return text.substr(at, c.length) === c; // nocommit - double check + }, + number = function () { + var number, + string = ''; + for ('-' === ch && ((string = '-'), next('-')); ch >= '0' && '9' >= ch; ) + (string += ch), next(); + if ('.' === ch) for (string += '.'; next() && ch >= '0' && '9' >= ch; ) string += ch; + if ('e' === ch || 'E' === ch) + for ( + string += ch, next(), ('-' === ch || '+' === ch) && ((string += ch), next()); + ch >= '0' && '9' >= ch; + + ) + (string += ch), next(); + return (number = +string), isNaN(number) ? (error('Bad number'), void 0) : number; + }, + string = function () { + let hex: any, + i: any, + uffff: any, + string = ''; + if ('"' === ch) { + if (peek('""')) { + // literal + next('"'); + next('"'); + return nextUpTo('"""', 'failed to find closing \'"""\''); + } else { + for (; next(); ) { + if ('"' === ch) return next(), string; + if ('\\' === ch) + if ((next(), 'u' === ch)) { + for ( + uffff = 0, i = 0; + 4 > i && ((hex = parseInt(next(), 16)), isFinite(hex)); + i += 1 + ) + uffff = 16 * uffff + hex; + string += String.fromCharCode(uffff); + } else { + if ('string' != typeof escapee[ch]) break; + string += escapee[ch]; + } + else string += ch; + } + } + } + error('Bad string'); + }, + white = function () { + for (; ch && ' ' >= ch; ) next(); + }, + word = function () { + switch (ch) { + case 't': + return next('t'), next('r'), next('u'), next('e'), !0; + case 'f': + return next('f'), next('a'), next('l'), next('s'), next('e'), !1; + case 'n': + return next('n'), next('u'), next('l'), next('l'), null; + } + error("Unexpected '" + ch + "'"); + }, + array = function () { + var array: any[] = []; + if ('[' === ch) { + if ((next('['), white(), ']' === ch)) return next(']'), array; + for (; ch; ) { + if ((array.push(value()), white(), ']' === ch)) return next(']'), array; + next(','), white(); + } + } + error('Bad array'); + }, + object = function () { + var key, + object: any = {}; + if ('{' === ch) { + if ((next('{'), white(), '}' === ch)) return next('}'), object; + for (; ch; ) { + let latchKeyStart = at; + if ( + ((key = string()), + white(), + next(':'), + Object.hasOwnProperty.call(object, key) && + warning('Duplicate key "' + key + '"', latchKeyStart), + (object[key] = value()), + white(), + '}' === ch) + ) + return next('}'), object; + next(','), white(); + } + } + error('Bad object'); + }; + return ( + (value = function () { + switch ((white(), ch)) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && '9' >= ch ? number() : word(); + } + }), + function (source: string) { + annos = []; + let errored = false; + text = source; + at = 0; + ch = ' '; + white(); + + try { + value(); + } catch (e) { + errored = true; + annos.push({ type: AnnoTypes.error, at: e.at - 1, text: e.message }); + } + if (!errored && ch) { + error('Syntax error'); + } + return { annotations: annos }; + } + ); +}; diff --git a/packages/kbn-monaco/src/xjson/index.ts b/packages/kbn-monaco/src/xjson/index.ts new file mode 100644 index 0000000000000..35fd35887bc56 --- /dev/null +++ b/packages/kbn-monaco/src/xjson/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { registerGrammarChecker } from './language'; + +import { ID } from './constants'; + +export const XJsonLang = { registerGrammarChecker, ID }; diff --git a/packages/kbn-monaco/src/xjson/language.ts b/packages/kbn-monaco/src/xjson/language.ts new file mode 100644 index 0000000000000..fe505818d3c9a --- /dev/null +++ b/packages/kbn-monaco/src/xjson/language.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// This file contains a lot of single setup logic for registering a language globally + +import { monaco } from '../monaco'; +import { WorkerProxyService } from './worker_proxy_service'; +import { registerLexerRules } from './lexer_rules'; +import { ID } from './constants'; +// @ts-ignore +import workerSrc from '!!raw-loader!../../target/public/xjson.editor.worker.js'; + +const wps = new WorkerProxyService(); + +// Register rules against shared monaco instance. +registerLexerRules(monaco); + +// In future we will need to make this map languages to workers using "id" and/or "label" values +// that get passed in. +// @ts-ignore +window.MonacoEnvironment = { + getWorker: (id: any, label: any) => { + // In kibana we will probably build this once and then load with raw-loader + const blob = new Blob([workerSrc], { type: 'application/javascript' }); + return new Worker(URL.createObjectURL(blob)); + }, +}; + +monaco.languages.onLanguage(ID, async () => { + return wps.setup(); +}); + +const OWNER = 'XJSON_GRAMMAR_CHECKER'; +export const registerGrammarChecker = (editor: monaco.editor.IEditor) => { + const allDisposables: monaco.IDisposable[] = []; + + const updateAnnos = async () => { + const { annotations } = await wps.getAnnos(); + const model = editor.getModel() as monaco.editor.ITextModel; + monaco.editor.setModelMarkers( + model, + OWNER, + annotations.map(({ at, text, type }) => { + const { column, lineNumber } = model.getPositionAt(at); + return { + startLineNumber: lineNumber, + startColumn: column, + endLineNumber: lineNumber, + endColumn: column, + message: text, + severity: type === 'error' ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning, + }; + }) + ); + }; + + const onModelAdd = (model: monaco.editor.IModel) => { + allDisposables.push( + model.onDidChangeContent(async () => { + updateAnnos(); + }) + ); + + updateAnnos(); + }; + + allDisposables.push(monaco.editor.onDidCreateModel(onModelAdd)); + monaco.editor.getModels().forEach(onModelAdd); + return () => { + wps.stop(); + allDisposables.forEach((d) => d.dispose()); + }; +}; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts b/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts new file mode 100644 index 0000000000000..e75b1013d3727 --- /dev/null +++ b/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts @@ -0,0 +1,270 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { monaco } from '../../monaco'; + +export const ID = 'esql'; + +const brackets = [ + { open: '[', close: ']', token: 'delimiter.square' }, + { open: '(', close: ')', token: 'delimiter.parenthesis' }, +]; + +const keywords = [ + 'describe', + 'between', + 'in', + 'like', + 'not', + 'and', + 'or', + 'desc', + 'select', + 'from', + 'where', + 'having', + 'group', + 'by', + 'order', + 'asc', + 'desc', + 'pivot', + 'for', + 'in', + 'as', + 'show', + 'columns', + 'include', + 'frozen', + 'tables', + 'escape', + 'limit', + 'rlike', + 'all', + 'distinct', + 'is', +]; +const builtinFunctions = [ + 'avg', + 'count', + 'first', + 'first_value', + 'last', + 'last_value', + 'max', + 'min', + 'sum', + 'kurtosis', + 'mad', + 'percentile', + 'percentile_rank', + 'skewness', + 'stddev_pop', + 'sum_of_squares', + 'var_pop', + 'histogram', + 'case', + 'coalesce', + 'greatest', + 'ifnull', + 'iif', + 'isnull', + 'least', + 'nullif', + 'nvl', + 'curdate', + 'current_date', + 'current_time', + 'current_timestamp', + 'curtime', + 'dateadd', + 'datediff', + 'datepart', + 'datetrunc', + 'date_add', + 'date_diff', + 'date_part', + 'date_trunc', + 'day', + 'dayname', + 'dayofmonth', + 'dayofweek', + 'dayofyear', + 'day_name', + 'day_of_month', + 'day_of_week', + 'day_of_year', + 'dom', + 'dow', + 'doy', + 'hour', + 'hour_of_day', + 'idow', + 'isodayofweek', + 'isodow', + 'isoweek', + 'isoweekofyear', + 'iso_day_of_week', + 'iso_week_of_year', + 'iw', + 'iwoy', + 'minute', + 'minute_of_day', + 'minute_of_hour', + 'month', + 'monthname', + 'month_name', + 'month_of_year', + 'now', + 'quarter', + 'second', + 'second_of_minute', + 'timestampadd', + 'timestampdiff', + 'timestamp_add', + 'timestamp_diff', + 'today', + 'week', + 'week_of_year', + 'year', + 'abs', + 'acos', + 'asin', + 'atan', + 'atan2', + 'cbrt', + 'ceil', + 'ceiling', + 'cos', + 'cosh', + 'cot', + 'degrees', + 'e', + 'exp', + 'expm1', + 'floor', + 'log', + 'log10', + 'mod', + 'pi', + 'power', + 'radians', + 'rand', + 'random', + 'round', + 'sign', + 'signum|sin', + 'sinh', + 'sqrt', + 'tan', + 'truncate', + 'ascii', + 'bit_length', + 'char', + 'character_length', + 'char_length', + 'concat', + 'insert', + 'lcase', + 'left', + 'length', + 'locate', + 'ltrim', + 'octet_length', + 'position', + 'repeat', + 'replace', + 'right', + 'rtrim', + 'space', + 'substring', + 'ucase', + 'cast', + 'convert', + 'database', + 'user', + 'st_astext', + 'st_aswkt', + 'st_distance', + 'st_geometrytype', + 'st_geomfromtext', + 'st_wkttosql', + 'st_x', + 'st_y', + 'st_z', + 'score', +]; + +export const lexerRules = { + defaultToken: 'invalid', + ignoreCase: true, + tokenPostfix: '', + keywords, + builtinFunctions, + brackets, + tokenizer: { + root: [ + [ + /[a-zA-Z_$][a-zA-Z0-9_$]*\b/, + { + cases: { + '@keywords': 'keyword', + '@builtinFunctions': 'identifier', + '@default': 'identifier', + }, + }, + ], + [/[()]/, '@brackets'], + [/--.*$/, 'comment'], + [/\/\*/, 'comment', '@comment'], + [/\/.*$/, 'comment'], + + [/".*?"/, 'string'], + + [/'.*?'/, 'constant'], + [/`.*?`/, 'string'], + // whitespace + [/[ \t\r\n]+/, { token: '@whitespace' }], + [/[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b/, 'entity.name.function'], + [/⇐|<⇒|\*|\.|\:\:|\+|\-|\/|\/\/|%|&|\^|~|<|>|<=|=>|==|!=|<>|=/, 'keyword.operator'], + [/[\(]/, 'paren.lparen'], + [/[\)]/, 'paren.rparen'], + [/\s+/, 'text'], + ], + numbers: [ + [/0[xX][0-9a-fA-F]*/, 'number'], + [/[$][+-]*\d*(\.\d*)?/, 'number'], + [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'], + ], + strings: [ + [/N'/, { token: 'string', next: '@string' }], + [/'/, { token: 'string', next: '@string' }], + ], + string: [ + [/[^']+/, 'string'], + [/''/, 'string'], + [/'/, { token: 'string', next: '@pop' }], + ], + comment: [ + [/[^\/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'], + ], + }, +} as monaco.languages.IMonarchLanguage; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/index.ts b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts new file mode 100644 index 0000000000000..515de09510a61 --- /dev/null +++ b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable-next-line @kbn/eslint/module_migration */ +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import * as xJson from './xjson'; +import * as esql from './esql'; +import * as painless from './painless'; + +export const registerLexerRules = (m: typeof monaco) => { + m.languages.register({ id: xJson.ID }); + m.languages.setMonarchTokensProvider(xJson.ID, xJson.lexerRules); + m.languages.register({ id: painless.ID }); + m.languages.setMonarchTokensProvider(painless.ID, painless.lexerRules); + m.languages.register({ id: esql.ID }); + m.languages.setMonarchTokensProvider(esql.ID, esql.lexerRules); +}; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts b/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts new file mode 100644 index 0000000000000..676eb3134026a --- /dev/null +++ b/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts @@ -0,0 +1,194 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { monaco } from '../../monaco'; + +export const ID = 'painless'; + +/** + * Extends the default type for a Monarch language so we can use + * attribute references (like @keywords to reference the keywords list) + * in the defined tokenizer + */ +interface Language extends monaco.languages.IMonarchLanguage { + default: string; + brackets: any; + keywords: string[]; + symbols: RegExp; + escapes: RegExp; + digits: RegExp; + primitives: string[]; + octaldigits: RegExp; + binarydigits: RegExp; + constants: string[]; + operators: string[]; +} + +export const lexerRules = { + default: 'invalid', + tokenPostfix: '', + // painless does not use < >, so we define our own + brackets: [ + ['{', '}', 'delimiter.curly'], + ['[', ']', 'delimiter.square'], + ['(', ')', 'delimiter.parenthesis'], + ], + keywords: [ + 'if', + 'in', + 'else', + 'while', + 'do', + 'for', + 'continue', + 'break', + 'return', + 'new', + 'try', + 'catch', + 'throw', + 'this', + 'instanceof', + ], + primitives: ['void', 'boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double', 'def'], + constants: ['true', 'false'], + operators: [ + '=', + '>', + '<', + '!', + '~', + '?', + '?:', + '?.', + ':', + '==', + '===', + '<=', + '>=', + '!=', + '!==', + '&&', + '||', + '++', + '--', + '+', + '-', + '*', + '/', + '&', + '|', + '^', + '%', + '<<', + '>>', + '>>>', + '+=', + '-=', + '*=', + '/=', + '&=', + '|=', + '^=', + '%=', + '<<=', + '>>=', + '>>>=', + '->', + '::', + '=~', + '==~', + ], + symbols: /[=> { + worker.initialize((ctx: any, createData: any) => { + return new XJsonWorker(ctx); + }); +}; diff --git a/packages/kbn-monaco/src/xjson/worker/xjson_worker.ts b/packages/kbn-monaco/src/xjson/worker/xjson_worker.ts new file mode 100644 index 0000000000000..501adcacb6990 --- /dev/null +++ b/packages/kbn-monaco/src/xjson/worker/xjson_worker.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable-next-line @kbn/eslint/module_migration */ +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { createParser } from '../grammar'; + +export class XJsonWorker { + constructor(private ctx: monaco.worker.IWorkerContext) {} + private parser: any; + + async parse() { + if (!this.parser) { + this.parser = createParser(); + } + const [model] = this.ctx.getMirrorModels(); + return this.parser(model.getValue()); + } +} diff --git a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts new file mode 100644 index 0000000000000..17d6d56e51e59 --- /dev/null +++ b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnnoTypes } from './grammar'; +import { monaco } from '../monaco'; +import { XJsonWorker } from './worker'; +import { ID } from './constants'; + +export interface Annotation { + name?: string; + type: AnnoTypes; + text: string; + at: number; +} + +export interface AnnotationsResponse { + annotations: Annotation[]; +} + +export class WorkerProxyService { + private worker: monaco.editor.MonacoWebWorker | undefined; + + public async getAnnos(): Promise { + if (!this.worker) { + throw new Error('Worker Proxy Service has not been setup!'); + } + await this.worker.withSyncedResources(monaco.editor.getModels().map(({ uri }) => uri)); + const proxy = await this.worker.getProxy(); + return proxy.parse(); + } + + public setup() { + this.worker = monaco.editor.createWebWorker({ label: ID, moduleId: '' }); + } + + public stop() { + if (this.worker) this.worker.dispose(); + } +} diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json new file mode 100644 index 0000000000000..95acfd32b24dd --- /dev/null +++ b/packages/kbn-monaco/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "declaration": true, + "sourceMap": true, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-monaco/webpack.config.js b/packages/kbn-monaco/webpack.config.js new file mode 100644 index 0000000000000..1a7d8c031670c --- /dev/null +++ b/packages/kbn-monaco/webpack.config.js @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const path = require('path'); + +const createLangWorkerConfig = (lang) => ({ + mode: 'production', + entry: path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`), + output: { + path: path.resolve(__dirname, 'target/public'), + filename: `${lang}.editor.worker.js`, + }, + resolve: { + modules: ['node_modules'], + extensions: ['.js', '.ts', '.tsx'], + }, + stats: 'errors-only', + module: { + rules: [ + { + test: /\.(js|ts)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, + }, + }, + ], + }, +}); + +module.exports = [createLangWorkerConfig('xjson')]; diff --git a/packages/kbn-monaco/yarn.lock b/packages/kbn-monaco/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-monaco/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 0ab0048619358..8e2fd1c9182ff 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -14,6 +14,7 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@types/parse-link-header": "^1.0.0", + "@types/puppeteer": "^3.0.0", "@types/strip-ansi": "^5.2.1", "@types/xml2js": "^0.4.5", "diff": "^4.0.1" @@ -25,6 +26,7 @@ "getopts": "^2.2.4", "glob": "^7.1.2", "parse-link-header": "^1.0.1", + "puppeteer": "^3.3.0", "strip-ansi": "^5.2.0", "rxjs": "^6.5.3", "tar-fs": "^1.16.3", diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 585ce8181df5f..0bc7cc664df68 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -58,3 +58,5 @@ export { runFailedTestsReporterCli } from './failed_tests_reporter'; export { makeJunitReportPath } from './junit_report_path'; export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; + +export * from './page_load_metrics'; diff --git a/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts b/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts new file mode 100644 index 0000000000000..013d49a29a51c --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog } from '@kbn/dev-utils'; +import { NavigationOptions, createUrl, navigateToApps } from './navigation'; + +export async function capturePageLoadMetrics(log: ToolingLog, options: NavigationOptions) { + const responsesByPageView = await navigateToApps(log, options); + + const assetSizeMeasurements = new Map(); + + const numberOfPagesVisited = responsesByPageView.size; + + for (const [, frameResponses] of responsesByPageView) { + for (const [, { url, dataLength }] of frameResponses) { + if (url.length === 0) { + throw new Error('navigateToApps(); failed to identify the url of the request'); + } + if (assetSizeMeasurements.has(url)) { + assetSizeMeasurements.set(url, [dataLength].concat(assetSizeMeasurements.get(url) || [])); + } else { + assetSizeMeasurements.set(url, [dataLength]); + } + } + } + + return Array.from(assetSizeMeasurements.entries()) + .map(([url, measurements]) => { + const baseUrl = createUrl('/', options.appConfig.url); + const relativeUrl = url + // remove the baseUrl (expect the trailing slash) to make url relative + .replace(baseUrl.slice(0, -1), '') + // strip the build number from asset urls + .replace(/^\/\d+\//, '/'); + return [relativeUrl, measurements] as const; + }) + .filter(([url, measurements]) => { + if (measurements.length !== numberOfPagesVisited) { + // ignore urls seen only on some pages + return false; + } + + if (url.startsWith('data:')) { + // ignore data urls since they are already counted by other assets + return false; + } + + if (url.startsWith('/api/') || url.startsWith('/internal/')) { + // ignore api requests since they don't have deterministic sizes + return false; + } + + const allMetricsAreEqual = measurements.every((x, i) => + i === 0 ? true : x === measurements[i - 1] + ); + if (!allMetricsAreEqual) { + throw new Error(`measurements for url [${url}] are not equal [${measurements.join(',')}]`); + } + + return true; + }) + .map(([url, measurements]) => { + return { group: 'page load asset size', id: url, value: measurements[0] }; + }); +} diff --git a/packages/kbn-test/src/page_load_metrics/cli.ts b/packages/kbn-test/src/page_load_metrics/cli.ts new file mode 100644 index 0000000000000..95421384c79cb --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/cli.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Url from 'url'; + +import { run, createFlagError } from '@kbn/dev-utils'; +import { resolve, basename } from 'path'; +import { capturePageLoadMetrics } from './capture_page_load_metrics'; + +const defaultScreenshotsDir = resolve(__dirname, 'screenshots'); + +export function runPageLoadMetricsCli() { + run( + async ({ flags, log }) => { + const kibanaUrl = flags['kibana-url']; + if (!kibanaUrl || typeof kibanaUrl !== 'string') { + throw createFlagError('Expect --kibana-url to be a string'); + } + + const parsedUrl = Url.parse(kibanaUrl); + + const [username, password] = parsedUrl.auth + ? parsedUrl.auth.split(':') + : [flags.username, flags.password]; + + if (typeof username !== 'string' || typeof password !== 'string') { + throw createFlagError( + 'Mising username and/or password, either specify in --kibana-url or pass --username and --password' + ); + } + + const headless = !flags.head; + + const screenshotsDir = flags.screenshotsDir || defaultScreenshotsDir; + + if (typeof screenshotsDir !== 'string' || screenshotsDir === basename(screenshotsDir)) { + throw createFlagError('Expect screenshotsDir to be valid path string'); + } + + const metrics = await capturePageLoadMetrics(log, { + headless, + appConfig: { + url: kibanaUrl, + username, + password, + }, + screenshotsDir, + }); + for (const metric of metrics) { + log.info(`${metric.id}: ${metric.value}`); + } + }, + { + description: `Loads several pages with Puppeteer to capture the size of assets`, + flags: { + string: ['kibana-url', 'username', 'password', 'screenshotsDir'], + boolean: ['head'], + default: { + username: 'elastic', + password: 'changeme', + debug: true, + screenshotsDir: defaultScreenshotsDir, + }, + help: ` + --kibana-url Url for Kibana we should connect to, can include login info + --head Run puppeteer with graphical user interface + --username Set username, defaults to 'elastic' + --password Set password, defaults to 'changeme' + --screenshotsDir Set screenshots directory, defaults to '${defaultScreenshotsDir}' + `, + }, + } + ); +} diff --git a/packages/kbn-test/src/page_load_metrics/event.ts b/packages/kbn-test/src/page_load_metrics/event.ts new file mode 100644 index 0000000000000..481954bbf672e --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/event.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface ResponseReceivedEvent { + frameId: string; + loaderId: string; + requestId: string; + response: Record; + timestamp: number; + type: string; +} + +export interface DataReceivedEvent { + encodedDataLength: number; + dataLength: number; + requestId: string; + timestamp: number; +} diff --git a/packages/kbn-test/src/page_load_metrics/index.ts b/packages/kbn-test/src/page_load_metrics/index.ts new file mode 100644 index 0000000000000..4309d558518a6 --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './cli'; +export { capturePageLoadMetrics } from './capture_page_load_metrics'; diff --git a/packages/kbn-test/src/page_load_metrics/navigation.ts b/packages/kbn-test/src/page_load_metrics/navigation.ts new file mode 100644 index 0000000000000..21dc681951b21 --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/navigation.ts @@ -0,0 +1,165 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fs from 'fs'; +import Url from 'url'; +import _ from 'lodash'; +import puppeteer from 'puppeteer'; +import { resolve } from 'path'; +import { ToolingLog } from '@kbn/dev-utils'; +import { ResponseReceivedEvent, DataReceivedEvent } from './event'; + +export interface NavigationOptions { + headless: boolean; + appConfig: { url: string; username: string; password: string }; + screenshotsDir: string; +} + +export type NavigationResults = Map>; + +interface FrameResponse { + url: string; + dataLength: number; +} + +function joinPath(pathA: string, pathB: string) { + return `${pathA.endsWith('/') ? pathA.slice(0, -1) : pathA}/${ + pathB.startsWith('/') ? pathB.slice(1) : pathB + }`; +} + +export function createUrl(path: string, url: string) { + const baseUrl = Url.parse(url); + return Url.format({ + protocol: baseUrl.protocol, + hostname: baseUrl.hostname, + port: baseUrl.port, + pathname: joinPath(baseUrl.pathname || '', path), + }); +} + +async function loginToKibana( + log: ToolingLog, + browser: puppeteer.Browser, + options: NavigationOptions +) { + log.debug(`log in to the app..`); + const page = await browser.newPage(); + const loginUrl = createUrl('/login', options.appConfig.url); + await page.goto(loginUrl, { + waitUntil: 'networkidle0', + }); + await page.type('[data-test-subj="loginUsername"]', options.appConfig.username); + await page.type('[data-test-subj="loginPassword"]', options.appConfig.password); + await page.click('[data-test-subj="loginSubmit"]'); + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + await page.close(); +} + +export async function navigateToApps(log: ToolingLog, options: NavigationOptions) { + const browser = await puppeteer.launch({ headless: options.headless, args: ['--no-sandbox'] }); + const devToolsResponses: NavigationResults = new Map(); + const apps = [ + { path: '/app/discover', locator: '[data-test-subj="discover-sidebar"]' }, + { path: '/app/home', locator: '[data-test-subj="homeApp"]' }, + { path: '/app/canvas', locator: '[data-test-subj="create-workpad-button"]' }, + { path: '/app/maps', locator: '[title="Maps"]' }, + { path: '/app/apm', locator: '[data-test-subj="apmMainContainer"]' }, + ]; + + await loginToKibana(log, browser, options); + + await Promise.all( + apps.map(async (app) => { + const page = await browser.newPage(); + page.setCacheEnabled(false); + page.setDefaultNavigationTimeout(0); + const frameResponses = new Map(); + devToolsResponses.set(app.path, frameResponses); + + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + + function getRequestData(requestId: string) { + if (!frameResponses.has(requestId)) { + frameResponses.set(requestId, { url: '', dataLength: 0 }); + } + + return frameResponses.get(requestId)!; + } + + client.on('Network.responseReceived', (event: ResponseReceivedEvent) => { + getRequestData(event.requestId).url = event.response.url; + }); + + client.on('Network.dataReceived', (event: DataReceivedEvent) => { + getRequestData(event.requestId).dataLength += event.dataLength; + }); + + const url = createUrl(app.path, options.appConfig.url); + log.debug(`goto ${url}`); + await page.goto(url, { + waitUntil: 'networkidle0', + }); + + let readyAttempt = 0; + let selectorFound = false; + while (!selectorFound) { + readyAttempt += 1; + try { + await page.waitForSelector(app.locator, { timeout: 5000 }); + selectorFound = true; + } catch (error) { + log.error( + `Page '${app.path}' was not loaded properly, unable to find '${ + app.locator + }', url: ${page.url()}` + ); + + if (readyAttempt < 6) { + continue; + } + + const failureDir = resolve(options.screenshotsDir, 'failure'); + const screenshotPath = resolve( + failureDir, + `${app.path.slice(1).split('/').join('_')}_navigation.png` + ); + Fs.mkdirSync(failureDir, { recursive: true }); + + await page.bringToFront(); + await page.screenshot({ + path: screenshotPath, + type: 'png', + fullPage: true, + }); + log.debug(`Saving screenshot to ${screenshotPath}`); + + throw new Error(`Page load timeout: ${app.path} not loaded after 30 seconds`); + } + } + + await page.close(); + }) + ); + + await browser.close(); + + return devToolsResponses; +} diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 26efd174f4e39..88e84fc87ae53 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -30,8 +30,8 @@ export const KbnI18nReact = require('@kbn/i18n/react'); export const Angular = require('angular'); export const Moment = require('moment'); export const MomentTimezone = require('moment-timezone/moment-timezone'); -export const Monaco = require('./monaco.ts'); -export const MonacoBare = require('monaco-editor/esm/vs/editor/editor.api'); +export const KbnMonaco = require('@kbn/monaco'); +export const MonacoBarePluginApi = require('@kbn/monaco').BarePluginApi; export const React = require('react'); export const ReactDom = require('react-dom'); export const ReactDomServer = require('react-dom/server'); @@ -44,6 +44,7 @@ Moment.tz.load(require('moment-timezone/data/packed/latest.json')); // big deps which are locked to a single version export const Rxjs = require('rxjs'); export const RxjsOperators = require('rxjs/operators'); +export const ElasticNumeral = require('@elastic/numeral'); export const ElasticCharts = require('@elastic/charts'); export const ElasticEui = require('@elastic/eui'); export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 9aec3ab359924..301d176555847 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -42,15 +42,17 @@ exports.externals = { 'react-intl': '__kbnSharedDeps__.ReactIntl', 'react-router': '__kbnSharedDeps__.ReactRouter', 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', - '@kbn/ui-shared-deps/monaco': '__kbnSharedDeps__.Monaco', + '@kbn/monaco': '__kbnSharedDeps__.KbnMonaco', // this is how plugins/consumers from npm load monaco - 'monaco-editor/esm/vs/editor/editor.api': '__kbnSharedDeps__.MonacoBare', + 'monaco-editor/esm/vs/editor/editor.api': '__kbnSharedDeps__.MonacoBarePluginApi', /** * big deps which are locked to a single version */ rxjs: '__kbnSharedDeps__.Rxjs', 'rxjs/operators': '__kbnSharedDeps__.RxjsOperators', + numeral: '__kbnSharedDeps__.ElasticNumeral', + '@elastic/numeral': '__kbnSharedDeps__.ElasticNumeral', '@elastic/charts': '__kbnSharedDeps__.ElasticCharts', '@elastic/eui': '__kbnSharedDeps__.ElasticEui', '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 4e6bec92a65e4..744a656c54a7f 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -11,7 +11,9 @@ "dependencies": { "@elastic/charts": "19.2.0", "@elastic/eui": "23.3.1", + "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", + "@kbn/monaco": "1.0.0", "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", "compression-webpack-plugin": "^3.1.0", @@ -21,7 +23,6 @@ "jquery": "^3.5.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", - "monaco-editor": "~0.17.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-intl": "^2.8.0", diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json index 5d981c73f1d21..5aa0f45e4100d 100644 --- a/packages/kbn-ui-shared-deps/tsconfig.json +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "../../tsconfig.json", - "include": [ - "index.d.ts", - "monaco.ts" - ] + "include": ["index.d.ts", "./monaco"] } diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 7295f2e88c530..523927bd64a20 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -78,17 +78,6 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, - { - include: [require.resolve('./monaco.ts')], - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }, - ], - }, ], }, @@ -96,6 +85,7 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ alias: { moment: MOMENT_SRC, }, + extensions: ['.js', '.ts'], }, optimization: { diff --git a/renovate.json5 b/renovate.json5 index 9a2ac20f91f04..674c4e0df7904 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -715,14 +715,6 @@ '@types/normalize-path', ], }, - { - groupSlug: 'numeral', - groupName: 'numeral related packages', - packageNames: [ - 'numeral', - '@types/numeral', - ], - }, { groupSlug: 'object-hash', groupName: 'object-hash related packages', diff --git a/rfcs/text/0011_global_search.md b/rfcs/text/0011_global_search.md index 5ec368a1c2f02..3b2120283d06a 100644 --- a/rfcs/text/0011_global_search.md +++ b/rfcs/text/0011_global_search.md @@ -194,7 +194,7 @@ Notes: ### Plugin API -#### server API +#### Common types ```ts /** @@ -208,6 +208,21 @@ type GlobalSearchResult = Omit & { url: string; }; + +/** + * Response returned from the {@link GlobalSearchServiceStart | global search service}'s `find` API + */ +type GlobalSearchBatchedResults = { + /** + * Results for this batch + */ + results: GlobalSearchResult[]; +}; +``` + +#### server API + +```ts /** * Options for the server-side {@link GlobalSearchServiceStart.find | find API} */ @@ -226,16 +241,6 @@ interface GlobalSearchFindOptions { aborted$?: Observable; } -/** - * Response returned from the server-side {@link GlobalSearchServiceStart | global search service}'s `find` API - */ -type GlobalSearchBatchedResults = { - /** - * Results for this batch - */ - results: GlobalSearchResult[]; -}; - /** @public */ interface GlobalSearchPluginSetup { registerResultProvider(provider: GlobalSearchResultProvider); @@ -265,28 +270,6 @@ interface GlobalSearchFindOptions { aborted$?: Observable; } -/** - * Enhanced {@link GlobalSearchResult | result type} for the client-side, - * to allow navigating to a given result. - */ -interface NavigableGlobalSearchResult extends GlobalSearchResult { - /** - * Navigate to this result's associated url. If the result is on this kibana instance, user will be redirected to it - * in a SPA friendly way using `application.navigateToApp`, else, a full page refresh will be performed. - */ - navigate: () => Promise; -} - -/** - * Response returned from the client-side {@link GlobalSearchServiceStart | global search service}'s `find` API - */ -type GlobalSearchBatchedResults = { - /** - * Results for this batch - */ - results: NavigableGlobalSearchResult[]; -}; - /** @public */ interface GlobalSearchPluginSetup { registerResultProvider(provider: GlobalSearchResultProvider); @@ -304,9 +287,6 @@ Notes: - The `registerResultProvider` setup APIs share the same signature, however the input `GlobalSearchResultProvider` types are different on the client and server. - The `find` start API signature got a `KibanaRequest` for `server`, when this parameter is not present for `public`. -- The `find` API returns a observable of `NavigableGlobalSearchResult` instead of plain `GlobalSearchResult`. This type - is here to enhance results with a `navigate` method to let the `GlobalSearch` plugin handle the navigation logic, which is - non-trivial. See the [Redirecting to a result](#redirecting-to-a-result) section for more info. #### http API @@ -395,14 +375,11 @@ In current specification, the only conversion step is to transform the `result.u #### redirecting to a result -Parsing a relative or absolute result url to perform SPA navigation can be non trivial, and should remains the responsibility -of the GlobalSearch plugin API. - -This is why `NavigableGlobalSearchResult.navigate` has been introduced on the client-side version of the `find` API +Parsing a relative or absolute result url to perform SPA navigation can be non trivial. This is why `ApplicationService.navigateToUrl` has been introduced on the client-side core API -When using `navigate` from a result instance, the following logic will be executed: +When using `navigateToUrl` with the url of a result instance, the following logic will be executed: -If all these criteria are true for `result.url`: +If all these criteria are true for `url`: - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location - The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space) diff --git a/scripts/page_load_metrics.js b/scripts/page_load_metrics.js new file mode 100644 index 0000000000000..37500c26e0b20 --- /dev/null +++ b/scripts/page_load_metrics.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('@kbn/test').runPageLoadMetricsCli(); diff --git a/scripts/prettier_on_changed.js b/scripts/prettier_on_changed.js deleted file mode 100644 index f9598110f91fd..0000000000000 --- a/scripts/prettier_on_changed.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -require('../src/setup_node_env/babel_register'); -require('../src/dev/run_prettier_on_changed'); diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index fc94f8d585a02..09f9bb2333dbe 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -261,8 +261,8 @@ export class ClusterManager { /debug\.log$/, ...pluginInternalDirsIgnore, fromRoot('src/legacy/server/sass/__tmp__'), - fromRoot('x-pack/legacy/plugins/reporting/.chromium'), - fromRoot('x-pack/plugins/siem/cypress'), + fromRoot('x-pack/plugins/reporting/.chromium'), + fromRoot('x-pack/plugins/security_solution/cypress'), fromRoot('x-pack/plugins/apm/e2e'), fromRoot('x-pack/plugins/apm/scripts'), fromRoot('x-pack/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 447c6f396945f..a4f50e73f1c57 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -167,17 +167,21 @@ leverage this pattern. import React from 'react'; import ReactDOM from 'react-dom'; -import { CoreStart, AppMountParams } from '../../src/core/public'; +import { CoreStart, AppMountParameters } from 'src/core/public'; import { MyAppRoot } from './components/app.ts'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ -export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, history }: AppMountParams) => { +export const renderApp = ( + core: CoreStart, + deps: MyPluginDepsStart, + { element, history }: AppMountParameters +) => { ReactDOM.render(, element); return () => ReactDOM.unmountComponentAtNode(element); -} +}; ``` ```ts @@ -332,7 +336,7 @@ import { SavedObjectsType } from 'src/core/server'; export const myType: SavedObjectsType = { name: 'my-type', hidden: false, - namespaceAgnostic: true, + namespaceType: 'single', mappings: { properties: { someField: { diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 5cec20fb900f2..6bb5a845ea2ab 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -850,7 +850,7 @@ import { SavedObjectsType } from 'src/core/server'; export const firstType: SavedObjectsType = { name: 'first-type', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { someField: { @@ -888,7 +888,7 @@ import { SavedObjectsType } from 'src/core/server'; export const secondType: SavedObjectsType = { name: 'second-type', hidden: true, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { textField: { @@ -936,7 +936,7 @@ export class MyPlugin implements Plugin { The NP `registerType` expected input is very close to the legacy format. However, there are some minor changes: -- The `schema.isNamespaceAgnostic` property has been renamed: `SavedObjectsType.namespaceAgnostic` +- The `schema.isNamespaceAgnostic` property has been renamed: `SavedObjectsType.namespaceType`. It no longer accepts a boolean but instead an enum of 'single', 'multiple', or 'agnostic' (see [SavedObjectsNamespaceType](/docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md)). - The `schema.indexPattern` was accepting either a `string` or a `(config: LegacyConfig) => string`. `SavedObjectsType.indexPattern` only accepts a string, as you can access the configuration during your plugin's setup phase. diff --git a/src/core/TESTING.md b/src/core/TESTING.md index cb38dac0e20ce..bed41ab583496 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -475,10 +475,14 @@ The more interesting logic is in `renderApp`: import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountParams, CoreStart } from 'src/core/public'; +import { AppMountParameters, CoreStart } from 'src/core/public'; import { AppRoot } from './components/app_root'; -export const renderApp = ({ element, history }: AppMountParams, core: CoreStart, plugins: MyPluginDepsStart) => { +export const renderApp = ( + { element, history }: AppMountParameters, + core: CoreStart, + plugins: MyPluginDepsStart +) => { // Hide the chrome while this app is mounted for a full screen experience core.chrome.setIsVisible(false); diff --git a/src/core/public/application/scoped_history.ts b/src/core/public/application/scoped_history.ts index 1a7fafa5d85c4..4392cf4eca8d4 100644 --- a/src/core/public/application/scoped_history.ts +++ b/src/core/public/application/scoped_history.ts @@ -197,7 +197,7 @@ export class ScopedHistory prompt?: boolean | string | TransitionPromptHook ): UnregisterCallback => { throw new Error( - `history.block is not supported. Please use the AppMountParams.onAppLeave API.` + `history.block is not supported. Please use the AppMountParameters.onAppLeave API.` ); }; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 2269fd0a4ca48..8006ec846138f 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -453,10 +453,10 @@ export interface AppMountParameters { * import ReactDOM from 'react-dom'; * import { BrowserRouter, Route } from 'react-router-dom'; * - * import { CoreStart, AppMountParams } from 'src/core/public'; + * import { CoreStart, AppMountParameters } from 'src/core/public'; * import { MyPluginDepsStart } from './plugin'; * - * export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { + * export renderApp = ({ element, history, onAppLeave }: AppMountParameters) => { * const { renderApp, hasUnsavedChanges } = await import('./application'); * onAppLeave(actions => { * if(hasUnsavedChanges()) { diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 7afb607192cae..f0db3a25e313d 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -17,7 +17,7 @@ * under the License. */ -import { map } from 'rxjs/operators'; +import { map, shareReplay } from 'rxjs/operators'; import { combineLatest } from 'rxjs'; import { CoreContext } from '../core_context'; import { PluginWrapper } from './plugin'; @@ -107,8 +107,8 @@ export function createPluginInitializerContext( * @param ConfigClass A class (not an instance of a class) that contains a * static `schema` that we validate the config at the given `path` against. */ - create() { - return coreContext.configService.atPath(pluginManifest.configPath); + create() { + return coreContext.configService.atPath(pluginManifest.configPath).pipe(shareReplay(1)); }, createIfExists() { return coreContext.configService.optionalAtPath(pluginManifest.configPath); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 4f69d45c192e9..69b57a498936e 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -74,6 +74,7 @@ export class KibanaMigrator { private readonly status$ = new BehaviorSubject({ status: 'waiting', }); + private readonly activeMappings: IndexMapping; /** * Creates an instance of KibanaMigrator. @@ -100,6 +101,9 @@ export class KibanaMigrator { validateDoc: docValidator(savedObjectValidations || {}), log: this.log, }); + // Building the active mappings (and associated md5sums) is an expensive + // operation so we cache the result + this.activeMappings = buildActiveMappings(this.mappingProperties); } /** @@ -172,7 +176,7 @@ export class KibanaMigrator { * */ public getActiveMappings(): IndexMapping { - return buildActiveMappings(this.mappingProperties); + return this.activeMappings; } /** diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts index 5b52665b6268e..28afdefe1413f 100644 --- a/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerBulkCreateRoute } from '../bulk_create'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts index 845bae47b41f2..521e62e16b1d8 100644 --- a/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerBulkGetRoute } from '../bulk_get'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts index 6356fc787a8d8..9c888406b0c96 100644 --- a/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerBulkUpdateRoute } from '../bulk_update'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/create.test.ts b/src/core/server/saved_objects/routes/integration_tests/create.test.ts index 5a53a30209281..ba3d620f8fdb5 100644 --- a/src/core/server/saved_objects/routes/integration_tests/create.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/create.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerCreateRoute } from '../create'; import { savedObjectsClientMock } from '../../service/saved_objects_client.mock'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts index d4ce4d421dde1..652d267f08fe7 100644 --- a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerDeleteRoute } from '../delete'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index bdb2e23f0826d..7b342dde2febe 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -27,7 +27,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { SavedObjectConfig } from '../../saved_objects_config'; import { registerExportRoute } from '../export'; -import { setupServer, createExportableType } from './test_utils'; +import { setupServer, createExportableType } from '../test_utils'; type setupServerReturn = UnwrapPromise>; const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock; diff --git a/src/core/server/saved_objects/routes/integration_tests/find.test.ts b/src/core/server/saved_objects/routes/integration_tests/find.test.ts index 7916100e46831..31bda1d6b9cbd 100644 --- a/src/core/server/saved_objects/routes/integration_tests/find.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/find.test.ts @@ -23,7 +23,7 @@ import querystring from 'querystring'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerFindRoute } from '../find'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index c4a03a0e2e7d2..c4e304a3f892f 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -22,7 +22,7 @@ import { UnwrapPromise } from '@kbn/utility-types'; import { registerImportRoute } from '../import'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { SavedObjectConfig } from '../../saved_objects_config'; -import { setupServer, createExportableType } from './test_utils'; +import { setupServer, createExportableType } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts b/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts index 4bbe3271e0232..0fe07245dda20 100644 --- a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerLogLegacyImportRoute } from '../log_legacy_import'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index a36f246f9dbc5..27750ec692e5a 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerResolveImportErrorsRoute } from '../resolve_import_errors'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer, createExportableType } from './test_utils'; +import { setupServer, createExportableType } from '../test_utils'; import { SavedObjectConfig } from '../../saved_objects_config'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/update.test.ts b/src/core/server/saved_objects/routes/integration_tests/update.test.ts index b0c3d68090db6..eb6eb1cdb6bd9 100644 --- a/src/core/server/saved_objects/routes/integration_tests/update.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/update.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerUpdateRoute } from '../update'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '../test_utils'; type setupServerReturn = UnwrapPromise>; diff --git a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts b/src/core/server/saved_objects/routes/test_utils.ts similarity index 83% rename from src/core/server/saved_objects/routes/integration_tests/test_utils.ts rename to src/core/server/saved_objects/routes/test_utils.ts index 23e0285201dc7..a2227a8033dbd 100644 --- a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts +++ b/src/core/server/saved_objects/routes/test_utils.ts @@ -17,14 +17,14 @@ * under the License. */ -import { ContextService } from '../../../context'; -import { createHttpServer, createCoreContext } from '../../../http/test_utils'; -import { coreMock } from '../../../mocks'; -import { SavedObjectsType } from '../../types'; +import { ContextService } from '../../context'; +import { createHttpServer, createCoreContext } from '../../http/test_utils'; +import { coreMock } from '../../mocks'; +import { SavedObjectsType } from '../types'; -const coreId = Symbol('core'); +const defaultCoreId = Symbol('core'); -export const setupServer = async () => { +export const setupServer = async (coreId: symbol = defaultCoreId) => { const coreContext = createCoreContext({ coreId }); const contextService = new ContextService(coreContext); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e23f8dec5927c..b093fe779cab7 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -136,7 +136,7 @@ export class SavedObjectsRepository { injectedConstructor: any = SavedObjectsRepository ): ISavedObjectsRepository { const mappings = migrator.getActiveMappings(); - const allTypes = Object.keys(getRootPropertiesObjects(mappings)); + const allTypes = typeRegistry.getAllTypes().map((t) => t.name); const serializer = new SavedObjectsSerializer(typeRegistry); const visibleTypes = allTypes.filter((type) => !typeRegistry.isHidden(type)); diff --git a/src/core/server/test_utils.ts b/src/core/server/test_utils.ts index f7e6fbcd0c131..6b16fe3bdef61 100644 --- a/src/core/server/test_utils.ts +++ b/src/core/server/test_utils.ts @@ -19,3 +19,4 @@ export { createHttpServer } from './http/test_utils'; export { ServiceStatusLevelSnapshotSerializer } from './status/test_utils'; +export { setupServer } from './saved_objects/routes/test_utils'; diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 1bea65ddee924..0eab40a7b3a5d 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -38,7 +38,7 @@ export const uiSettingsType: SavedObjectsType = { importableAndExportable: true, getInAppUrl() { return { - path: `/app/kibana#/management/kibana/settings`, + path: `/app/management/kibana/settings`, uiCapabilitiesPath: 'advancedSettings.show', }; }, diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 346b4cfce70c1..07d7789d235ec 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -26,3 +26,4 @@ export * from './capabilities'; export * from './app_category'; export * from './ui_settings'; export * from './saved_objects'; +export * from './serializable'; diff --git a/src/core/types/serializable.ts b/src/core/types/serializable.ts new file mode 100644 index 0000000000000..9e8ea123bea91 --- /dev/null +++ b/src/core/types/serializable.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type Serializable = + | string + | number + | boolean + | null + | SerializableArray + | SerializableRecord; + +// we need interfaces instead of types here to allow cyclic references +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SerializableArray extends Array {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SerializableRecord extends Record {} diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index 3a8709893565d..66f0c0355c2d9 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -30,6 +30,7 @@ import { CleanTypescriptTask, CleanNodeBuildsTask, CleanTask, + CopyBinScriptsTask, CopySourceTask, CreateArchivesSourcesTask, CreateArchivesTask, @@ -110,6 +111,7 @@ export async function buildDistributables(options) { * run platform-generic build tasks */ await run(CopySourceTask); + await run(CopyBinScriptsTask); await run(CreateEmptyDirsAndFilesTask); await run(CreateReadmeTask); await run(TranspileBabelTask); diff --git a/src/dev/build/tasks/bin/copy_bin_scripts_task.js b/src/dev/build/tasks/bin/copy_bin_scripts_task.js new file mode 100644 index 0000000000000..f620f12b17d88 --- /dev/null +++ b/src/dev/build/tasks/bin/copy_bin_scripts_task.js @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { copyAll } from '../../lib'; + +export const CopyBinScriptsTask = { + description: 'Copying bin scripts into platform-generic build directory', + + async run(config, log, build) { + await copyAll( + config.resolveFromRepo('src/dev/build/tasks/bin/scripts'), + build.resolvePath('bin') + ); + }, +}; diff --git a/src/dev/build/tasks/bin/index.js b/src/dev/build/tasks/bin/index.js new file mode 100644 index 0000000000000..e970ac5ec044b --- /dev/null +++ b/src/dev/build/tasks/bin/index.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { CopyBinScriptsTask } from './copy_bin_scripts_task'; diff --git a/bin/kibana b/src/dev/build/tasks/bin/scripts/kibana similarity index 100% rename from bin/kibana rename to src/dev/build/tasks/bin/scripts/kibana diff --git a/bin/kibana-keystore b/src/dev/build/tasks/bin/scripts/kibana-keystore similarity index 100% rename from bin/kibana-keystore rename to src/dev/build/tasks/bin/scripts/kibana-keystore diff --git a/bin/kibana-keystore.bat b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat similarity index 100% rename from bin/kibana-keystore.bat rename to src/dev/build/tasks/bin/scripts/kibana-keystore.bat diff --git a/bin/kibana-plugin b/src/dev/build/tasks/bin/scripts/kibana-plugin similarity index 100% rename from bin/kibana-plugin rename to src/dev/build/tasks/bin/scripts/kibana-plugin diff --git a/bin/kibana-plugin.bat b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat similarity index 100% rename from bin/kibana-plugin.bat rename to src/dev/build/tasks/bin/scripts/kibana-plugin.bat diff --git a/bin/kibana.bat b/src/dev/build/tasks/bin/scripts/kibana.bat similarity index 100% rename from bin/kibana.bat rename to src/dev/build/tasks/bin/scripts/kibana.bat diff --git a/src/dev/build/tasks/clean_tasks.js b/src/dev/build/tasks/clean_tasks.js index ec38dd8a1195c..31731e392e5cb 100644 --- a/src/dev/build/tasks/clean_tasks.js +++ b/src/dev/build/tasks/clean_tasks.js @@ -206,7 +206,7 @@ export const CleanExtraBrowsersTask = { async run(config, log, build) { const getBrowserPathsForPlatform = (platform) => { - const reportingDir = 'x-pack/legacy/plugins/reporting'; + const reportingDir = 'x-pack/plugins/reporting'; const chromiumDir = '.chromium'; const chromiumPath = (p) => build.resolvePathForPlatform(platform, reportingDir, chromiumDir, p); diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index ee9dc159de47f..ddc6d000bca19 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -42,7 +42,6 @@ export const CopySourceTask = { '!src/es_archiver/**', '!src/functional_test_runner/**', '!src/dev/**', - 'bin/**', 'typings/**', 'webpackShims/**', 'config/kibana.yml', diff --git a/src/dev/build/tasks/index.js b/src/dev/build/tasks/index.js index 8105fa8a7d5d4..bafb5a2fe115e 100644 --- a/src/dev/build/tasks/index.js +++ b/src/dev/build/tasks/index.js @@ -17,6 +17,7 @@ * under the License. */ +export * from './bin'; export * from './build_packages_task'; export * from './clean_tasks'; export * from './copy_source_task'; diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service index 1d88ee535fefa..e66e0e7c8dfb5 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service @@ -10,7 +10,7 @@ Group=kibana # exist, it continues onward. EnvironmentFile=-/etc/default/kibana EnvironmentFile=-/etc/sysconfig/kibana -ExecStart=/usr/share/kibana/bin/kibana "-c /etc/kibana/kibana.yml" +ExecStart=/usr/share/kibana/bin/kibana Restart=on-failure RestartSec=3 StartLimitBurst=3 diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index a17d15522b45e..ce29fd12b8e3c 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -17,7 +17,6 @@ name=kibana program=/usr/share/kibana/bin/kibana -args=-c\\\ /etc/kibana/kibana.yml pidfile="/var/run/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name @@ -53,7 +52,7 @@ start() { chroot --userspec "$user":"$group" "$chroot" sh -c " cd \"$chdir\" - exec \"$program\" $args + exec \"$program\" " >> /var/log/kibana/kibana.stdout 2>> /var/log/kibana/kibana.stderr & # Generate the pidfile from here. If we instead made the forked process diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index c62a28a2956e1..8c982b792ed3b 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -35,13 +35,13 @@ describe(`Transform fn`, () => { it(`should remove the jenkins workspace path`, () => { const obj = { staticSiteUrl: - '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/legacy/plugins/reporting/server/browsers/extract/unzip.js', + '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', COVERAGE_INGESTION_KIBANA_ROOT: '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', }; expect(coveredFilePath(obj)).to.have.property( 'coveredFilePath', - 'x-pack/legacy/plugins/reporting/server/browsers/extract/unzip.js' + 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' ); }); }); diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-manual-mix.json b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-manual-mix.json index baf2b6d500679..25cd2fdfb259d 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-manual-mix.json +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-manual-mix.json @@ -1,5 +1,5 @@ { - "/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/legacy/plugins/reporting/server/browsers/extract/unzip.js": { + "/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js": { "lines": { "total": 4, "covered": 4, diff --git a/src/dev/code_coverage/nyc_config/nyc.functional.config.js b/src/dev/code_coverage/nyc_config/nyc.functional.config.js new file mode 100644 index 0000000000000..20d266ab9e2c3 --- /dev/null +++ b/src/dev/code_coverage/nyc_config/nyc.functional.config.js @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const defaultExclude = require('@istanbuljs/schema/default-exclude'); +const extraExclude = ['data/optimize/**', 'src/core/server/**', '**/test/**']; +const path = require('path'); + +module.exports = { + 'temp-dir': process.env.COVERAGE_TEMP_DIR + ? path.resolve(process.env.COVERAGE_TEMP_DIR, 'functional') + : 'target/kibana-coverage/functional', + 'report-dir': 'target/kibana-coverage/functional-combined', + reporter: ['html', 'json-summary'], + exclude: extraExclude.concat(defaultExclude), +}; diff --git a/src/dev/code_coverage/nyc_config/nyc.jest.config.js b/src/dev/code_coverage/nyc_config/nyc.jest.config.js new file mode 100644 index 0000000000000..1f73347837ab3 --- /dev/null +++ b/src/dev/code_coverage/nyc_config/nyc.jest.config.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const path = require('path'); + +module.exports = { + 'temp-dir': process.env.COVERAGE_TEMP_DIR + ? path.resolve(process.env.COVERAGE_TEMP_DIR, 'jest') + : 'target/kibana-coverage/jest', + 'report-dir': 'target/kibana-coverage/jest-combined', + reporter: ['html', 'json-summary'], +}; diff --git a/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh b/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh index bc184301a6831..098737eb2f800 100644 --- a/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh +++ b/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh @@ -7,7 +7,13 @@ COMBINED_EXRACT_DIR=/${EXTRACT_START_DIR}/${EXTRACT_END_DIR} PWD=$(pwd) du -sh $COMBINED_EXRACT_DIR -echo "### Replacing path in json files" +echo "### Jest: replacing path in json files" +for i in coverage-final xpack-coverage-final; do + sed -i "s|/dev/shm/workspace/kibana|${PWD}|g" $COMBINED_EXRACT_DIR/jest/${i}.json & +done +wait + +echo "### Functional: replacing path in json files" for i in {1..9}; do sed -i "s|/dev/shm/workspace/kibana|${PWD}|g" $COMBINED_EXRACT_DIR/functional/${i}*.json & done diff --git a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh index ff9cb36c894f8..707c6de3f88a0 100644 --- a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh +++ b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh @@ -1,10 +1,9 @@ #!/bin/bash -EXTRACT_START_DIR=tmp/extracted_coverage -EXTRACT_END_DIR=target/kibana-coverage -COMBINED_EXTRACT_DIR=/${EXTRACT_START_DIR}/${EXTRACT_END_DIR} +COVERAGE_TEMP_DIR=/tmp/extracted_coverage/target/kibana-coverage/ +export COVERAGE_TEMP_DIR echo "### Merge coverage reports" for x in jest functional; do - yarn nyc report --temp-dir $COMBINED_EXTRACT_DIR/${x} --report-dir $EXTRACT_END_DIR/${x}-combined --reporter=html --reporter=json-summary + yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.${x}.config.js done diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 497a639ecab56..64db131f5219a 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -64,7 +64,8 @@ export default { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js', - '\\.ace\\.worker.js$': '/src/dev/jest/mocks/ace_worker_module_mock.js', + '\\.ace\\.worker.js$': '/src/dev/jest/mocks/worker_module_mock.js', + '\\.editor\\.worker.js$': '/src/dev/jest/mocks/worker_module_mock.js', '^(!!)?file-loader!': '/src/dev/jest/mocks/file_mock.js', }, setupFiles: [ diff --git a/src/dev/jest/mocks/ace_worker_module_mock.js b/src/dev/jest/mocks/worker_module_mock.js similarity index 100% rename from src/dev/jest/mocks/ace_worker_module_mock.js rename to src/dev/jest/mocks/worker_module_mock.js diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index d968a365e7bb9..253fc104061a8 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -173,12 +173,12 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/plugins/monitoring/public/icons/health-green.svg', 'x-pack/plugins/monitoring/public/icons/health-red.svg', 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', - 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/data.json.gz', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json', 'x-pack/test/functional/es_archives/monitoring/logstash-pipelines/data.json.gz', diff --git a/src/dev/run_prettier_on_changed.ts b/src/dev/run_prettier_on_changed.ts deleted file mode 100644 index 776d4d5ffd389..0000000000000 --- a/src/dev/run_prettier_on_changed.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import execa from 'execa'; -// @ts-ignore -import SimpleGit from 'simple-git'; -import { run } from '@kbn/dev-utils'; -import dedent from 'dedent'; -import Util from 'util'; - -import pkg from '../../package.json'; -import { REPO_ROOT } from './constants'; -import { File } from './file'; -import * as Eslint from './eslint'; - -run(async function getChangedFiles({ log }) { - const simpleGit = new SimpleGit(REPO_ROOT); - - const getStatus = Util.promisify(simpleGit.status.bind(simpleGit)); - const gitStatus = await getStatus(); - - if (gitStatus.files.length > 0) { - throw new Error( - dedent(`You should run prettier formatter on a clean branch. - Found not committed changes to: - ${gitStatus.files.map((f: { path: string }) => f.path).join('\n')}`) - ); - } - - const revParse = Util.promisify(simpleGit.revparse.bind(simpleGit)); - const currentBranch = await revParse(['--abbrev-ref', 'HEAD']); - const headBranch = pkg.branch; - - const diff = Util.promisify(simpleGit.diff.bind(simpleGit)); - - const changedFileStatuses: string = await diff([ - '--name-status', - `${headBranch}...${currentBranch}`, - ]); - - const changedFiles = changedFileStatuses - .split('\n') - // Ignore blank lines - .filter((line) => line.trim().length > 0) - // git diff --name-status outputs lines with two OR three parts - // separated by a tab character - .map((line) => line.trim().split('\t')) - .map(([status, ...paths]) => { - // ignore deleted files - if (status === 'D') { - return undefined; - } - - // the status is always in the first column - // .. If the file is edited the line will only have two columns - // .. If the file is renamed it will have three columns - // .. In any case, the last column is the CURRENT path to the file - return new File(paths[paths.length - 1]); - }) - .filter((file): file is File => Boolean(file)); - - const pathsToLint = Eslint.pickFilesToLint(log, changedFiles).map((f) => f.getAbsolutePath()); - - if (pathsToLint.length > 0) { - log.debug('[prettier] run on %j files: ', pathsToLint.length, pathsToLint); - } - - while (pathsToLint.length > 0) { - await execa('npx', ['prettier@2.0.4', '--write', ...pathsToLint.splice(0, 100)]); - } -}); diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 416702c56d852..2f785896da8d5 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,7 +18,6 @@ */ export const storybookAliases = { - advanced_ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', apm: 'x-pack/plugins/apm/scripts/storybook.js', canvas: 'x-pack/plugins/canvas/scripts/storybook_new.js', codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', @@ -26,5 +25,6 @@ export const storybookAliases = { drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', - siem: 'x-pack/plugins/siem/scripts/storybook.js', + security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js', + ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/scripts/storybook.js', }; diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index b368949cc33e1..1e0b631308d9e 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -27,8 +27,8 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/siem/cypress/tsconfig.json'), { - name: 'siem/cypress', + new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), { + name: 'security_solution/cypress', }), new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { name: 'apm/cypress', diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index ef56ae0e2380c..ae613e0e80904 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -26,8 +26,7 @@ import { exportApi } from './server/routes/api/export'; import { getUiSettingDefaults } from './server/ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; import { injectVars } from './inject_vars'; -import { i18n } from '@kbn/i18n'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; + import { kbnBaseUrl } from '../../../plugins/kibana_legacy/server'; const mkdirAsync = promisify(Fs.mkdir); @@ -53,19 +52,7 @@ export default function (kibana) { main: 'plugins/kibana/kibana', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - links: [ - { - id: 'kibana:stack_management', - title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Stack Management', - }), - order: 9003, - url: `${kbnBaseUrl}#/management`, - euiIconType: 'managementApp', - linkToLastSubUrl: false, - category: DEFAULT_APP_CATEGORIES.management, - }, - ], + links: [], injectDefaultVars(server, options) { const mapConfig = server.config().get('map'); diff --git a/src/legacy/core_plugins/kibana/public/.eslintrc.js b/src/legacy/core_plugins/kibana/public/.eslintrc.js deleted file mode 100644 index 1153706eb8566..0000000000000 --- a/src/legacy/core_plugins/kibana/public/.eslintrc.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const topLevelConfig = require('../../../../../.eslintrc.js'); -const path = require('path'); - -const topLevelRestricedZones = topLevelConfig.overrides.find( - override => - override.files[0] === '**/*.{js,ts,tsx}' && - Object.keys(override.rules)[0] === '@kbn/eslint/no-restricted-paths' -).rules['@kbn/eslint/no-restricted-paths'][1].zones; - -/** - * Builds custom restricted paths configuration for the shimmed plugins within the kibana plugin. - * These custom rules extend the default checks in the top level `eslintrc.js` by also checking two other things: - * * Making sure nothing within np_ready imports from the `ui` directory - * * Making sure no other code is importing things deep from within the shimmed plugins - * @param shimmedPlugins List of plugin names within the kibana plugin that are partially np ready - * @returns zones configuration for the no-restricted-paths linter - */ -function buildRestrictedPaths(shimmedPlugins) { - return shimmedPlugins - .map(shimmedPlugin => [ - { - target: [`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/np_ready/**/*`], - from: [ - 'ui/**/*', - 'src/legacy/ui/**/*', - 'src/legacy/core_plugins/kibana/public/**/*', - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, - ], - allowSameFolder: false, - errorMessage: `${shimmedPlugin} is a shimmed plugin that is not allowed to import modules from the legacy platform. If you need legacy modules for the transition period, import them either in the legacy_imports, kibana_services or index module.`, - }, - { - target: [ - 'src/**/*', - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, - 'x-pack/**/*', - ], - from: [ - `src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`, - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`, - ], - allowSameFolder: false, - errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`, - }, - ]) - .reduce((acc, part) => [...acc, ...part], []); -} - -module.exports = { - rules: { - 'no-console': 2, - 'import/no-default-export': 'error', - '@kbn/eslint/no-restricted-paths': [ - 'error', - { - basePath: path.resolve(__dirname, '../../../../../'), - zones: topLevelRestricedZones.concat( - buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools']) - ), - }, - ], - }, -}; diff --git a/src/legacy/core_plugins/kibana/public/_hacks.scss b/src/legacy/core_plugins/kibana/public/_hacks.scss deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 0bf74edc77cb6..51dedcc629c76 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -38,17 +38,22 @@ import 'uiExports/shareContextMenuExtensions'; import 'uiExports/interpreter'; import 'ui/autoload/all'; -import './management'; + import { localApplicationService } from './local_application_service'; npSetup.plugins.kibanaLegacy.registerLegacyAppAlias('doc', 'discover', { keepPrefix: true }); npSetup.plugins.kibanaLegacy.registerLegacyAppAlias('context', 'discover', { keepPrefix: true }); +npSetup.plugins.kibanaLegacy.forwardApp('management', 'management', (path) => { + return path.replace('/management', ''); +}); + localApplicationService.attachToAngular(routes); routes.enable(); const { config } = npSetup.plugins.kibanaLegacy; + routes.otherwise({ redirectTo: `/${config.defaultAppId || 'discover'}`, }); diff --git a/src/legacy/core_plugins/kibana/public/management/_hacks.scss b/src/legacy/core_plugins/kibana/public/management/_hacks.scss deleted file mode 100644 index 59af9c9617a30..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/_hacks.scss +++ /dev/null @@ -1,29 +0,0 @@ -// SASSTODO: figure out why this is needed -kbn-management-app, -kbn-management-landing, -kbn-management-indices, -kbn-management-indices-edit, -kbn-management-indices-create, -kbn-management-advanced, -kbn-management-objects, -kbn-management-objects-view { - display: block; -} - -#management-landing { - display: flex; -} - -.kbn-management-tab:first-letter { - text-transform: capitalize; -} - -// SASSTODO: Remove when this is replaced with EuiCode -kbn-management-objects-view { - .ace_editor { height: 300px; } -} - -// Hack because the management wrapper is flat HTML and needs a class -.mgtPage__body { - max-width: map-get($euiBreakpoints, 'xl'); -} diff --git a/src/legacy/core_plugins/kibana/public/management/_management_app.scss b/src/legacy/core_plugins/kibana/public/management/_management_app.scss deleted file mode 100644 index bd3cabbc574d3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/_management_app.scss +++ /dev/null @@ -1,69 +0,0 @@ -.mgtPanel { - margin-bottom: $euiSize; - background: $euiColorEmptyShade; -} - -/** - * 1. Override kuiPanelBody styles to accommodate padding of items within the panel body.. - */ -.mgtPanel__body { - padding: 5px 10px; /* 1 */ -} - -/** - * 1. Create vertical space between items when they wrap. - */ -.mgtPanel__item { - padding: 5px 15px; /* 1 */ -} - -// SASSTODO: Remove when this is replaced by the side nav -.mgtPanel__link { - @include euiFontSizeL; - - line-height: 1.5; // Make sure the space between wrapped lines is than the vertical space between items. - - &.mgtPanel__link--disabled { - opacity: $euiColorDarkShade; - cursor: default; - - &:hover, &:visited { - color: $euiColorPrimary; - } - } -} - -// SASSTODO: Remove when this form is replaced by EUI -kbn-management-objects { - form { - margin-bottom: $euiSize; - } - .list-unstyled { - li { - border-bottom: $euiBorderThin; - padding: $euiSizeS; - } - } - .empty { - color: $euiColorDarkShade; - } - - .item { - padding: $euiSizeM; - - .item-title { - margin-left: $euiSizeL; - } - - .actions { - margin-top: $euiSizeXS; - } - } - - .header { - .title, .controls { - padding-right: 1em; - display: inline-block; - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/app.html b/src/legacy/core_plugins/kibana/public/management/app.html deleted file mode 100644 index 11198c02960c7..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/app.html +++ /dev/null @@ -1,4 +0,0 @@ -

-
-
-
diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js deleted file mode 100644 index 48f0e2517a486..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import uiRoutes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -import appTemplate from './app.html'; -import landingTemplate from './landing.html'; -import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { ManagementSidebarNav } from '../../../../../plugins/management/public'; -import { timefilter } from 'ui/timefilter'; -import { - EuiPageContent, - EuiTitle, - EuiText, - EuiSpacer, - EuiIcon, - EuiHorizontalRule, -} from '@elastic/eui'; -import { npStart } from 'ui/new_platform'; - -const SIDENAV_ID = 'management-sidenav'; -const LANDING_ID = 'management-landing'; - -uiRoutes.when('/management', { - template: landingTemplate, - k7Breadcrumbs: () => [MANAGEMENT_BREADCRUMB], -}); - -uiRoutes.when('/management/:section', { - redirectTo: '/management', -}); - -export function updateLandingPage(version) { - const node = document.getElementById(LANDING_ID); - if (!node) { - return; - } - - render( - - -
-
- - - -

- -

-
- - - -
- - - - -

- -

-
-
-
-
, - node - ); -} - -export function updateSidebar(legacySections, id) { - const node = document.getElementById(SIDENAV_ID); - if (!node) { - return; - } - - render( - - - , - node - ); -} - -export const destroyReact = (id) => { - const node = document.getElementById(id); - node && unmountComponentAtNode(node); -}; - -uiModules.get('apps/management').directive('kbnManagementApp', function ($location) { - return { - restrict: 'E', - template: appTemplate, - transclude: true, - scope: { - sectionName: '@section', - omitPages: '@omitBreadcrumbPages', - pageTitle: '=', - }, - - link: function ($scope) { - timefilter.disableAutoRefreshSelector(); - timefilter.disableTimeRangeSelector(); - $scope.sections = management.visibleItems; - $scope.section = management.getSection($scope.sectionName) || management; - - if ($scope.section) { - $scope.section.items.forEach((item) => { - item.active = `#${$location.path()}`.indexOf(item.url) > -1; - }); - } - - updateSidebar($scope.sections, $scope.section.id); - $scope.$on('$destroy', () => destroyReact(SIDENAV_ID)); - management.addListener(() => updateSidebar(management.visibleItems, $scope.section.id)); - - updateLandingPage($scope.$root.chrome.getKibanaVersion()); - $scope.$on('$destroy', () => destroyReact(LANDING_ID)); - }, - }; -}); - -uiModules.get('apps/management').directive('kbnManagementLanding', function (kbnVersion) { - return { - restrict: 'E', - link: function ($scope) { - $scope.sections = management.visibleItems; - $scope.kbnVersion = kbnVersion; - }, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/index.scss b/src/legacy/core_plugins/kibana/public/management/index.scss index 123580c0b7907..fb267b714f1c9 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.scss +++ b/src/legacy/core_plugins/kibana/public/management/index.scss @@ -7,9 +7,7 @@ // mgtChart__legend--small // mgtChart__legend-isLoading -@import 'hacks'; - // Core -@import 'management_app'; @import '../../../../../plugins/advanced_settings/public/index'; + @import 'sections/index_patterns/index'; diff --git a/src/legacy/core_plugins/kibana/public/management/landing.html b/src/legacy/core_plugins/kibana/public/management/landing.html deleted file mode 100644 index 39459b26f7415..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/landing.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts deleted file mode 100644 index 587a372f91555..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { npSetup } from 'ui/new_platform'; - -const registry = npSetup.plugins.savedObjectsManagement?.serviceRegistry; - -export const savedObjectManagementRegistry = registry!; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/types.ts deleted file mode 100644 index 81184d6fdd1a3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface IndexPatternCreationOption { - text: string; - description?: string; - onClick: () => void; -} - -export interface IndexPattern { - id: string; - title: string; - url: string; - active: boolean; - default: boolean; - tag?: string[]; - sort: string; -} diff --git a/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js b/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js index 64676b1bce75c..4df0e7a140205 100644 --- a/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js +++ b/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js @@ -69,7 +69,7 @@ const savedObjectsManagement = getManagementaMock({ }, getInAppUrl(obj) { return { - path: `/app/kibana#/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`, + path: `/app/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`, uiCapabilitiesPath: 'management.kibana.index_patterns', }; }, @@ -325,7 +325,7 @@ describe('findRelationships', () => { title: 'My Index Pattern', editUrl: '/management/kibana/indexPatterns/patterns/1', inAppUrl: { - path: '/app/kibana#/management/kibana/indexPatterns/patterns/1', + path: '/app/management/kibana/indexPatterns/patterns/1', uiCapabilitiesPath: 'management.kibana.index_patterns', }, }, @@ -439,7 +439,7 @@ describe('findRelationships', () => { title: 'My Index Pattern', editUrl: '/management/kibana/indexPatterns/patterns/1', inAppUrl: { - path: '/app/kibana#/management/kibana/indexPatterns/patterns/1', + path: '/app/management/kibana/indexPatterns/patterns/1', uiCapabilitiesPath: 'management.kibana.index_patterns', }, }, diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 0d1b69778263c..b7af6a73e1bc1 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -18,53 +18,32 @@ */ import moment from 'moment-timezone'; -import numeralLanguages from '@elastic/numeral/languages'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { isRelativeUrl } from '../../../../core/server'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../../plugins/data/common'; export function getUiSettingDefaults() { const weekdays = moment.weekdays().slice(); const [defaultWeekday] = weekdays; - // We add the `en` key manually here, since that's not a real numeral locale, but the - // default fallback in case the locale is not found. - const numeralLanguageIds = [ - 'en', - ...numeralLanguages.map(function (numeralLanguage) { - return numeralLanguage.id; - }), - ]; - - const luceneQueryLanguageLabel = i18n.translate( - 'kbn.advancedSettings.searchQueryLanguageLucene', - { - defaultMessage: 'Lucene', - } - ); - - const queryLanguageSettingName = i18n.translate('kbn.advancedSettings.searchQueryLanguageTitle', { - defaultMessage: 'Query language', - }); - - const requestPreferenceOptionLabels = { - sessionId: i18n.translate('kbn.advancedSettings.courier.requestPreferenceSessionId', { - defaultMessage: 'Session ID', - }), - custom: i18n.translate('kbn.advancedSettings.courier.requestPreferenceCustom', { - defaultMessage: 'Custom', - }), - none: i18n.translate('kbn.advancedSettings.courier.requestPreferenceNone', { - defaultMessage: 'None', - }), - }; // wrapped in provider so that a new instance is given to each app/test return { buildNum: { readonly: true, }, + 'state:storeInSessionStorage': { + name: i18n.translate('kbn.advancedSettings.storeUrlTitle', { + defaultMessage: 'Store URLs in session storage', + }), + value: false, + description: i18n.translate('kbn.advancedSettings.storeUrlText', { + defaultMessage: + 'The URL can sometimes grow to be too large for some browsers to handle. ' + + 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + + 'Please let us know how it goes!', + }), + }, defaultRoute: { name: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteTitle', { defaultMessage: 'Default route', @@ -89,83 +68,6 @@ export function getUiSettingDefaults() { 'The route must be a relative URL.', }), }, - 'query:queryString:options': { - name: i18n.translate('kbn.advancedSettings.query.queryStringOptionsTitle', { - defaultMessage: 'Query string options', - }), - value: '{ "analyze_wildcard": true }', - description: i18n.translate('kbn.advancedSettings.query.queryStringOptionsText', { - defaultMessage: - '{optionsLink} for the lucene query string parser. Is only used when "{queryLanguage}" is set ' + - 'to {luceneLanguage}.', - description: - 'Part of composite text: kbn.advancedSettings.query.queryStringOptions.optionsLinkText + ' + - 'kbn.advancedSettings.query.queryStringOptionsText', - values: { - optionsLink: - '' + - i18n.translate('kbn.advancedSettings.query.queryStringOptions.optionsLinkText', { - defaultMessage: 'Options', - }) + - '', - luceneLanguage: luceneQueryLanguageLabel, - queryLanguage: queryLanguageSettingName, - }, - }), - type: 'json', - }, - 'query:allowLeadingWildcards': { - name: i18n.translate('kbn.advancedSettings.query.allowWildcardsTitle', { - defaultMessage: 'Allow leading wildcards in query', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.query.allowWildcardsText', { - defaultMessage: - 'When set, * is allowed as the first character in a query clause. ' + - 'Currently only applies when experimental query features are enabled in the query bar. ' + - 'To disallow leading wildcards in basic lucene queries, use {queryStringOptionsPattern}.', - values: { - queryStringOptionsPattern: 'query:queryString:options', - }, - }), - }, - 'search:queryLanguage': { - name: queryLanguageSettingName, - value: DEFAULT_QUERY_LANGUAGE, - description: i18n.translate('kbn.advancedSettings.searchQueryLanguageText', { - defaultMessage: - 'Query language used by the query bar. KQL is a new language built specifically for Kibana.', - }), - type: 'select', - options: ['lucene', 'kuery'], - optionLabels: { - lucene: luceneQueryLanguageLabel, - kuery: i18n.translate('kbn.advancedSettings.searchQueryLanguageKql', { - defaultMessage: 'KQL', - }), - }, - }, - 'sort:options': { - name: i18n.translate('kbn.advancedSettings.sortOptionsTitle', { - defaultMessage: 'Sort options', - }), - value: '{ "unmapped_type": "boolean" }', - description: i18n.translate('kbn.advancedSettings.sortOptionsText', { - defaultMessage: '{optionsLink} for the Elasticsearch sort parameter', - description: - 'Part of composite text: kbn.advancedSettings.sortOptions.optionsLinkText + ' + - 'kbn.advancedSettings.sortOptionsText', - values: { - optionsLink: - '' + - i18n.translate('kbn.advancedSettings.sortOptions.optionsLinkText', { - defaultMessage: 'Options', - }) + - '', - }, - }), - type: 'json', - }, dateFormat: { name: i18n.translate('kbn.advancedSettings.dateFormatTitle', { defaultMessage: 'Date format', @@ -261,160 +163,6 @@ export function getUiSettingDefaults() { }, }), }, - defaultIndex: { - name: i18n.translate('kbn.advancedSettings.defaultIndexTitle', { - defaultMessage: 'Default index', - }), - value: null, - type: 'string', - description: i18n.translate('kbn.advancedSettings.defaultIndexText', { - defaultMessage: 'The index to access if no index is set', - }), - }, - 'courier:ignoreFilterIfFieldNotInIndex': { - name: i18n.translate('kbn.advancedSettings.courier.ignoreFilterTitle', { - defaultMessage: 'Ignore filter(s)', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.courier.ignoreFilterText', { - defaultMessage: - 'This configuration enhances support for dashboards containing visualizations accessing dissimilar indexes. ' + - 'When disabled, all filters are applied to all visualizations. ' + - 'When enabled, filter(s) will be ignored for a visualization ' + - `when the visualization's index does not contain the filtering field.`, - }), - category: ['search'], - }, - 'courier:setRequestPreference': { - name: i18n.translate('kbn.advancedSettings.courier.requestPreferenceTitle', { - defaultMessage: 'Request preference', - }), - value: 'sessionId', - options: ['sessionId', 'custom', 'none'], - optionLabels: requestPreferenceOptionLabels, - type: 'select', - description: i18n.translate('kbn.advancedSettings.courier.requestPreferenceText', { - defaultMessage: `Allows you to set which shards handle your search requests. -
    -
  • {sessionId}: restricts operations to execute all search requests on the same shards. - This has the benefit of reusing shard caches across requests.
  • -
  • {custom}: allows you to define a your own preference. - Use courier:customRequestPreference to customize your preference value.
  • -
  • {none}: means do not set a preference. - This might provide better performance because requests can be spread across all shard copies. - However, results might be inconsistent because different shards might be in different refresh states.
  • -
`, - values: { - sessionId: requestPreferenceOptionLabels.sessionId, - custom: requestPreferenceOptionLabels.custom, - none: requestPreferenceOptionLabels.none, - }, - }), - category: ['search'], - }, - 'courier:customRequestPreference': { - name: i18n.translate('kbn.advancedSettings.courier.customRequestPreferenceTitle', { - defaultMessage: 'Custom request preference', - }), - value: '_local', - type: 'string', - description: i18n.translate('kbn.advancedSettings.courier.customRequestPreferenceText', { - defaultMessage: - '{requestPreferenceLink} used when {setRequestReferenceSetting} is set to {customSettingValue}.', - description: - 'Part of composite text: kbn.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText + ' + - 'kbn.advancedSettings.courier.customRequestPreferenceText', - values: { - setRequestReferenceSetting: 'courier:setRequestPreference', - customSettingValue: '"custom"', - requestPreferenceLink: - '' + - i18n.translate( - 'kbn.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText', - { - defaultMessage: 'Request Preference', - } - ) + - '', - }, - }), - category: ['search'], - }, - 'courier:maxConcurrentShardRequests': { - name: i18n.translate('kbn.advancedSettings.courier.maxRequestsTitle', { - defaultMessage: 'Max Concurrent Shard Requests', - }), - value: 0, - type: 'number', - description: i18n.translate('kbn.advancedSettings.courier.maxRequestsText', { - defaultMessage: - 'Controls the {maxRequestsLink} setting used for _msearch requests sent by Kibana. ' + - 'Set to 0 to disable this config and use the Elasticsearch default.', - values: { - maxRequestsLink: `max_concurrent_shard_requests`, - }, - }), - category: ['search'], - }, - 'courier:batchSearches': { - name: i18n.translate('kbn.advancedSettings.courier.batchSearchesTitle', { - defaultMessage: 'Batch concurrent searches', - }), - value: false, - type: 'boolean', - description: i18n.translate('kbn.advancedSettings.courier.batchSearchesText', { - defaultMessage: `When disabled, dashboard panels will load individually, and search requests will terminate when users navigate - away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, and - searches will not terminate.`, - }), - deprecation: { - message: i18n.translate('kbn.advancedSettings.courier.batchSearchesTextDeprecation', { - defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', - }), - docLinksKey: 'kibanaSearchSettings', - }, - category: ['search'], - }, - 'search:includeFrozen': { - name: 'Search in frozen indices', - description: `Will include frozen indices in results if enabled. Searching through frozen indices - might increase the search time.`, - value: false, - category: ['search'], - }, - 'histogram:barTarget': { - name: i18n.translate('kbn.advancedSettings.histogram.barTargetTitle', { - defaultMessage: 'Target bars', - }), - value: 50, - description: i18n.translate('kbn.advancedSettings.histogram.barTargetText', { - defaultMessage: - 'Attempt to generate around this many bars when using "auto" interval in date histograms', - }), - }, - 'histogram:maxBars': { - name: i18n.translate('kbn.advancedSettings.histogram.maxBarsTitle', { - defaultMessage: 'Maximum bars', - }), - value: 100, - description: i18n.translate('kbn.advancedSettings.histogram.maxBarsText', { - defaultMessage: - 'Never show more than this many bars in date histograms, scale values if needed', - }), - }, - 'visualize:enableLabs': { - name: i18n.translate('kbn.advancedSettings.visualizeEnableLabsTitle', { - defaultMessage: 'Enable experimental visualizations', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.visualizeEnableLabsText', { - defaultMessage: `Allows users to create, view, and edit experimental visualizations. If disabled, - only visualizations that are considered production-ready are available to the user.`, - }), - category: ['visualization'], - }, 'visualization:tileMap:maxPrecision': { name: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle', { defaultMessage: 'Maximum tile map precision', @@ -493,43 +241,6 @@ export function getUiSettingDefaults() { }), category: ['visualization'], }, - 'csv:separator': { - name: i18n.translate('kbn.advancedSettings.csv.separatorTitle', { - defaultMessage: 'CSV separator', - }), - value: ',', - description: i18n.translate('kbn.advancedSettings.csv.separatorText', { - defaultMessage: 'Separate exported values with this string', - }), - }, - 'csv:quoteValues': { - name: i18n.translate('kbn.advancedSettings.csv.quoteValuesTitle', { - defaultMessage: 'Quote CSV values', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.csv.quoteValuesText', { - defaultMessage: 'Should values be quoted in csv exports?', - }), - }, - 'history:limit': { - name: i18n.translate('kbn.advancedSettings.historyLimitTitle', { - defaultMessage: 'History limit', - }), - value: 10, - description: i18n.translate('kbn.advancedSettings.historyLimitText', { - defaultMessage: - 'In fields that have history (e.g. query inputs), show this many recent values', - }), - }, - 'shortDots:enable': { - name: i18n.translate('kbn.advancedSettings.shortenFieldsTitle', { - defaultMessage: 'Shorten fields', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.shortenFieldsText', { - defaultMessage: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz', - }), - }, 'truncate:maxHeight': { name: i18n.translate('kbn.advancedSettings.maxCellHeightTitle', { defaultMessage: 'Maximum table cell height', @@ -540,138 +251,6 @@ export function getUiSettingDefaults() { 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', }), }, - 'format:defaultTypeMap': { - name: i18n.translate('kbn.advancedSettings.format.defaultTypeMapTitle', { - defaultMessage: 'Field type format name', - }), - value: `{ - "ip": { "id": "ip", "params": {} }, - "date": { "id": "date", "params": {} }, - "date_nanos": { "id": "date_nanos", "params": {}, "es": true }, - "number": { "id": "number", "params": {} }, - "boolean": { "id": "boolean", "params": {} }, - "_source": { "id": "_source", "params": {} }, - "_default_": { "id": "string", "params": {} } -}`, - type: 'json', - description: i18n.translate('kbn.advancedSettings.format.defaultTypeMapText', { - defaultMessage: - 'Map of the format name to use by default for each field type. ' + - '{defaultFormat} is used if the field type is not mentioned explicitly', - values: { - defaultFormat: '"_default_"', - }, - }), - }, - 'format:number:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.numberFormatTitle', { - defaultMessage: 'Number format', - }), - value: '0,0.[000]', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.numberFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "number" format', - description: - 'Part of composite text: kbn.advancedSettings.format.numberFormatText + ' + - 'kbn.advancedSettings.format.numberFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.numberFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:bytes:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.bytesFormatTitle', { - defaultMessage: 'Bytes format', - }), - value: '0,0.[0]b', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.bytesFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "bytes" format', - description: - 'Part of composite text: kbn.advancedSettings.format.bytesFormatText + ' + - 'kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:percent:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.percentFormatTitle', { - defaultMessage: 'Percent format', - }), - value: '0,0.[000]%', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.percentFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "percent" format', - description: - 'Part of composite text: kbn.advancedSettings.format.percentFormatText + ' + - 'kbn.advancedSettings.format.percentFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.percentFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:currency:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.currencyFormatTitle', { - defaultMessage: 'Currency format', - }), - value: '($0,0.[00])', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.currencyFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "currency" format', - description: - 'Part of composite text: kbn.advancedSettings.format.currencyFormatText + ' + - 'kbn.advancedSettings.format.currencyFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.currencyFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:number:defaultLocale': { - name: i18n.translate('kbn.advancedSettings.format.formattingLocaleTitle', { - defaultMessage: 'Formatting locale', - }), - value: 'en', - type: 'select', - options: numeralLanguageIds, - optionLabels: Object.fromEntries( - numeralLanguages.map((language) => [language.id, language.name]) - ), - description: i18n.translate('kbn.advancedSettings.format.formattingLocaleText', { - defaultMessage: `{numeralLanguageLink} locale`, - description: - 'Part of composite text: kbn.advancedSettings.format.formattingLocale.numeralLanguageLinkText + ' + - 'kbn.advancedSettings.format.formattingLocaleText', - values: { - numeralLanguageLink: - '' + - i18n.translate('kbn.advancedSettings.format.formattingLocale.numeralLanguageLinkText', { - defaultMessage: 'Numeral language', - }) + - '', - }, - }), - }, 'timepicker:timeDefaults': { name: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsTitle', { defaultMessage: 'Time filter defaults', @@ -686,120 +265,6 @@ export function getUiSettingDefaults() { }), requiresPageReload: true, }, - 'timepicker:refreshIntervalDefaults': { - name: i18n.translate('kbn.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { - defaultMessage: 'Time filter refresh interval', - }), - value: `{ - "pause": false, - "value": 0 -}`, - type: 'json', - description: i18n.translate('kbn.advancedSettings.timepicker.refreshIntervalDefaultsText', { - defaultMessage: `The timefilter's default refresh interval`, - }), - requiresPageReload: true, - }, - 'timepicker:quickRanges': { - name: i18n.translate('kbn.advancedSettings.timepicker.quickRangesTitle', { - defaultMessage: 'Time filter quick ranges', - }), - value: JSON.stringify( - [ - { - from: 'now/d', - to: 'now/d', - display: i18n.translate('kbn.advancedSettings.timepicker.today', { - defaultMessage: 'Today', - }), - }, - { - from: 'now/w', - to: 'now/w', - display: i18n.translate('kbn.advancedSettings.timepicker.thisWeek', { - defaultMessage: 'This week', - }), - }, - { - from: 'now-15m', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last15Minutes', { - defaultMessage: 'Last 15 minutes', - }), - }, - { - from: 'now-30m', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last30Minutes', { - defaultMessage: 'Last 30 minutes', - }), - }, - { - from: 'now-1h', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last1Hour', { - defaultMessage: 'Last 1 hour', - }), - }, - { - from: 'now-24h', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last24Hours', { - defaultMessage: 'Last 24 hours', - }), - }, - { - from: 'now-7d', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last7Days', { - defaultMessage: 'Last 7 days', - }), - }, - { - from: 'now-30d', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last30Days', { - defaultMessage: 'Last 30 days', - }), - }, - { - from: 'now-90d', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last90Days', { - defaultMessage: 'Last 90 days', - }), - }, - { - from: 'now-1y', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last1Year', { - defaultMessage: 'Last 1 year', - }), - }, - ], - null, - 2 - ), - type: 'json', - description: i18n.translate('kbn.advancedSettings.timepicker.quickRangesText', { - defaultMessage: - 'The list of ranges to show in the Quick section of the time filter. This should be an array of objects, ' + - 'with each object containing "from", "to" (see {acceptedFormatsLink}), and ' + - '"display" (the title to be displayed).', - description: - 'Part of composite text: kbn.advancedSettings.timepicker.quickRangesText + ' + - 'kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', - values: { - acceptedFormatsLink: - `` + - i18n.translate('kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', { - defaultMessage: 'accepted formats', - }) + - '', - }, - }), - }, 'theme:darkMode': { name: i18n.translate('kbn.advancedSettings.darkModeTitle', { defaultMessage: 'Dark mode', @@ -822,26 +287,6 @@ export function getUiSettingDefaults() { }), requiresPageReload: true, }, - 'filters:pinnedByDefault': { - name: i18n.translate('kbn.advancedSettings.pinFiltersTitle', { - defaultMessage: 'Pin filters by default', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.pinFiltersText', { - defaultMessage: 'Whether the filters should have a global state (be pinned) by default', - }), - }, - 'filterEditor:suggestValues': { - name: i18n.translate('kbn.advancedSettings.suggestFilterValuesTitle', { - defaultMessage: 'Filter editor suggest values', - description: '"Filter editor" refers to the UI you create filters in.', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.suggestFilterValuesText', { - defaultMessage: - 'Set this property to false to prevent the filter editor from suggesting values for fields.', - }), - }, 'notifications:banner': { name: i18n.translate('kbn.advancedSettings.notifications.bannerTitle', { defaultMessage: 'Custom banner notification', @@ -930,28 +375,6 @@ export function getUiSettingDefaults() { type: 'number', category: ['notifications'], }, - 'state:storeInSessionStorage': { - name: i18n.translate('kbn.advancedSettings.storeUrlTitle', { - defaultMessage: 'Store URLs in session storage', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.storeUrlText', { - defaultMessage: - 'The URL can sometimes grow to be too large for some browsers to handle. ' + - 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + - 'Please let us know how it goes!', - }), - }, - 'indexPattern:placeholder': { - name: i18n.translate('kbn.advancedSettings.indexPatternPlaceholderTitle', { - defaultMessage: 'Index pattern placeholder', - }), - value: '', - description: i18n.translate('kbn.advancedSettings.indexPatternPlaceholderText', { - defaultMessage: - 'The placeholder for the "Index pattern name" field in "Management > Index Patterns > Create Index Pattern".', - }), - }, 'accessibility:disableAnimations': { name: i18n.translate('kbn.advancedSettings.disableAnimationsTitle', { defaultMessage: 'Disable Animations', diff --git a/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap b/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap index b88210758a00d..7d4b245021c4c 100644 --- a/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap +++ b/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap @@ -4,7 +4,7 @@ exports[`byte metric 1`] = ` `; diff --git a/src/legacy/core_plugins/status_page/public/components/metric_tiles.test.js b/src/legacy/core_plugins/status_page/public/components/metric_tiles.test.js index 74dfbd4119f14..13d0a61bbc96f 100644 --- a/src/legacy/core_plugins/status_page/public/components/metric_tiles.test.js +++ b/src/legacy/core_plugins/status_page/public/components/metric_tiles.test.js @@ -47,20 +47,20 @@ const MS_METRIC = { test('general metric', () => { const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line + expect(component).toMatchSnapshot(); }); test('byte metric', () => { const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line + expect(component).toMatchSnapshot(); }); test('float metric', () => { const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line + expect(component).toMatchSnapshot(); }); test('millisecond metric', () => { const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line + expect(component).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/status_page/public/lib/format_number.js b/src/legacy/core_plugins/status_page/public/lib/format_number.js index c5f23a9a9ef6d..4a8be4fc48a15 100644 --- a/src/legacy/core_plugins/status_page/public/lib/format_number.js +++ b/src/legacy/core_plugins/status_page/public/lib/format_number.js @@ -17,7 +17,7 @@ * under the License. */ -import numeral from 'numeral'; +import numeral from '@elastic/numeral'; export default function formatNumber(num, which) { let format = '0.00'; diff --git a/src/legacy/core_plugins/status_page/public/lib/format_number.test.js b/src/legacy/core_plugins/status_page/public/lib/format_number.test.js index 78f17ffa76f39..f70377dcba241 100644 --- a/src/legacy/core_plugins/status_page/public/lib/format_number.test.js +++ b/src/legacy/core_plugins/status_page/public/lib/format_number.test.js @@ -21,42 +21,42 @@ import formatNumber from './format_number'; describe('format byte', () => { test('zero', () => { - expect(formatNumber(0, 'byte')).toEqual('0.00 B'); + expect(formatNumber(0, 'byte')).toMatchInlineSnapshot(`"0.00 B"`); }); test('mb', () => { - expect(formatNumber(181142512, 'byte')).toEqual('181.14 MB'); + expect(formatNumber(181142512, 'byte')).toMatchInlineSnapshot(`"172.75 MB"`); }); test('gb', () => { - expect(formatNumber(273727485000, 'byte')).toEqual('273.73 GB'); + expect(formatNumber(273727485000, 'byte')).toMatchInlineSnapshot(`"254.93 GB"`); }); }); describe('format ms', () => { test('zero', () => { - expect(formatNumber(0, 'ms')).toEqual('0.00 ms'); + expect(formatNumber(0, 'ms')).toMatchInlineSnapshot(`"0.00 ms"`); }); test('sub ms', () => { - expect(formatNumber(0.128, 'ms')).toEqual('0.13 ms'); + expect(formatNumber(0.128, 'ms')).toMatchInlineSnapshot(`"0.13 ms"`); }); test('many ms', () => { - expect(formatNumber(3030.284, 'ms')).toEqual('3030.28 ms'); + expect(formatNumber(3030.284, 'ms')).toMatchInlineSnapshot(`"3030.28 ms"`); }); }); describe('format integer', () => { test('zero', () => { - expect(formatNumber(0, 'integer')).toEqual('0'); + expect(formatNumber(0, 'integer')).toMatchInlineSnapshot(`"0"`); }); test('sub integer', () => { - expect(formatNumber(0.728, 'integer')).toEqual('1'); + expect(formatNumber(0.728, 'integer')).toMatchInlineSnapshot(`"1"`); }); test('many integer', () => { - expect(formatNumber(3030.284, 'integer')).toEqual('3030'); + expect(formatNumber(3030.284, 'integer')).toMatchInlineSnapshot(`"3030"`); }); }); diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js index 08a347fbf7295..879fab206b99d 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js @@ -29,6 +29,7 @@ import { PaginateDirectiveProvider, } from '../../../../../plugins/kibana_legacy/public'; import { PER_PAGE_SETTING } from '../../../../../plugins/saved_objects/common'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../../plugins/visualizations/public'; const module = uiModules.get('kibana'); @@ -294,7 +295,7 @@ module prevSearch = filter; - const isLabsEnabled = config.get('visualize:enableLabs'); + const isLabsEnabled = config.get(VISUALIZE_ENABLE_LABS_SETTING); self.service.find(filter).then(function (hits) { hits.hits = hits.hits.filter( (hit) => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental' diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 7d84c27bd1ef0..63839b9d0f1d7 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -27,14 +27,13 @@ import { importSavedObjectsFromStream, resolveSavedObjectsImportErrors, } from '../../../core/server/saved_objects'; -import { getRootPropertiesObjects } from '../../../core/server/saved_objects/mappings'; import { convertTypesToLegacySchema } from '../../../core/server/saved_objects/utils'; export function savedObjectsMixin(kbnServer, server) { const migrator = kbnServer.newPlatform.__internals.kibanaMigrator; const typeRegistry = kbnServer.newPlatform.start.core.savedObjects.getTypeRegistry(); const mappings = migrator.getActiveMappings(); - const allTypes = Object.keys(getRootPropertiesObjects(mappings)); + const allTypes = typeRegistry.getAllTypes().map((t) => t.name); const schema = new SavedObjectsSchema(convertTypesToLegacySchema(typeRegistry.getAllTypes())); const visibleTypes = allTypes.filter((type) => !schema.isHiddenType(type)); diff --git a/src/legacy/ui/public/management/index.d.ts b/src/legacy/ui/public/management/index.d.ts deleted file mode 100644 index 529efd36623a3..0000000000000 --- a/src/legacy/ui/public/management/index.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -declare module 'ui/management' { - export const SidebarNav: React.FC; - export const management: any; // TODO - properly provide types - export const MANAGEMENT_BREADCRUMB: { - text: string; - href: string; - }; -} diff --git a/src/legacy/ui/public/management/index.js b/src/legacy/ui/public/management/index.js deleted file mode 100644 index 25d3678c5dbba..0000000000000 --- a/src/legacy/ui/public/management/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { MANAGEMENT_BREADCRUMB } from './breadcrumbs'; -import { npStart } from 'ui/new_platform'; -export const management = npStart.plugins.management.legacy; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 229bfb1978a4e..d98770842a0f0 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -28,6 +28,11 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../src/plugins/data/public/search/aggs'; import { ComponentRegistry } from '../../../../../src/plugins/advanced_settings/public/'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public/'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../src/plugins/share/public'; const mockObservable = () => { return { @@ -49,18 +54,31 @@ let isTimeRangeSelectorEnabled = true; let isAutoRefreshSelectorEnabled = true; export const mockUiSettings = { - get: (item) => { - return mockUiSettings[item]; + get: (item, defaultValue) => { + const defaultValues = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + 'dateFormat:tz': 'UTC', + [UI_SETTINGS.SHORT_DOTS_ENABLE]: true, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: true, + [UI_SETTINGS.QUERY_STRING_OPTIONS]: {}, + [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', + [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, + [CSV_SEPARATOR_SETTING]: ',', + [CSV_QUOTE_VALUES_SETTING]: true, + [UI_SETTINGS.SEARCH_QUERY_LANGUAGE]: 'kuery', + 'state:storeInSessionStorage': false, + }; + + return defaultValues[item] || defaultValue; }, getUpdate$: () => ({ subscribe: sinon.fake(), }), isDefault: sinon.fake(), - 'query:allowLeadingWildcards': true, - 'query:queryString:options': {}, - 'courier:ignoreFilterIfFieldNotInIndex': true, - 'dateFormat:tz': 'Browser', - 'format:defaultTypeMap': {}, }; const mockCoreSetup = { @@ -236,6 +254,9 @@ export const npSetup = { }, share: { register: () => {}, + urlGenerators: { + registerUrlGenerator: () => {}, + }, }, devTools: { register: () => {}, @@ -524,6 +545,8 @@ export function __setup__(coreSetup) { // bootstrap an LP plugin outside of tests) npSetup.core.application.register = () => {}; + npSetup.core.uiSettings.get = mockUiSettings.get; + // Services that need to be set in the legacy platform since the legacy data // & vis plugins which previously provided them have been removed. setSetupServices(npSetup); @@ -532,6 +555,8 @@ export function __setup__(coreSetup) { export function __start__(coreStart) { npStart.core = coreStart; + npStart.core.uiSettings.get = mockUiSettings.get; + // Services that need to be set in the legacy platform since the legacy data // & vis plugins which previously provided them have been removed. setStartServices(npStart); diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts index 9d02ad67b3937..ee92eda064aa8 100644 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ b/src/legacy/ui/public/new_platform/set_services.ts @@ -65,6 +65,7 @@ export function setStartServices(npStart: NpStart) { visualizationsServices.setCapabilities(npStart.core.application.capabilities); visualizationsServices.setHttp(npStart.core.http); visualizationsServices.setApplication(npStart.core.application); + visualizationsServices.setEmbeddable(npStart.plugins.embeddable); visualizationsServices.setSavedObjects(npStart.core.savedObjects); visualizationsServices.setIndexPatterns(npStart.plugins.data.indexPatterns); visualizationsServices.setFilterManager(npStart.plugins.data.query.filterManager); diff --git a/src/legacy/ui/public/timefilter/setup_router.ts b/src/legacy/ui/public/timefilter/setup_router.ts index a7492e538b3af..7c25c6aa3166e 100644 --- a/src/legacy/ui/public/timefilter/setup_router.ts +++ b/src/legacy/ui/public/timefilter/setup_router.ts @@ -21,10 +21,15 @@ import _ from 'lodash'; import { IScope } from 'angular'; import moment from 'moment'; import chrome from 'ui/chrome'; -import { RefreshInterval, TimeRange, TimefilterContract } from 'src/plugins/data/public'; import { Subscription } from 'rxjs'; import { fatalError } from 'ui/notify/fatal_error'; import { subscribeWithScope } from '../../../../plugins/kibana_legacy/public'; +import { + RefreshInterval, + TimeRange, + TimefilterContract, + UI_SETTINGS, +} from '../../../../plugins/data/public'; // TODO // remove everything underneath once globalState is no longer an angular service @@ -38,7 +43,7 @@ export function getTimefilterConfig() { const settings = chrome.getUiSettingsClient(); return { timeDefaults: settings.get('timepicker:timeDefaults'), - refreshIntervalDefaults: settings.get('timepicker:refreshIntervalDefaults'), + refreshIntervalDefaults: settings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; } diff --git a/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts b/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts index bf3ac9628754a..31df6875c97d9 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts @@ -46,8 +46,8 @@ const names: Record = { search: i18n.translate('advancedSettings.categoryNames.searchLabel', { defaultMessage: 'Search', }), - siem: i18n.translate('advancedSettings.categoryNames.siemLabel', { - defaultMessage: 'SIEM', + securitySolution: i18n.translate('advancedSettings.categoryNames.securitySolutionLabel', { + defaultMessage: 'Security Solution', }), }; diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index df44ea45e9d01..b4779d051ab02 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -19,7 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route } from 'react-router-dom'; +import { Router, Switch, Route } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; @@ -60,7 +60,7 @@ export async function mountManagementSection( ReactDOM.render( - + - + , params.element ); diff --git a/src/plugins/charts/public/services/theme/theme.ts b/src/plugins/charts/public/services/theme/theme.ts index 166e1c539688a..e1e71573caa3a 100644 --- a/src/plugins/charts/public/services/theme/theme.ts +++ b/src/plugins/charts/public/services/theme/theme.ts @@ -42,8 +42,10 @@ export class ThemeService { /** A React hook for consuming the charts theme */ public useChartsTheme = () => { + /* eslint-disable-next-line react-hooks/rules-of-hooks */ const [value, update] = useState(this.chartsDefaultTheme); + /* eslint-disable-next-line react-hooks/rules-of-hooks */ useEffect(() => { const s = this.chartsTheme$.subscribe(update); return () => s.unsubscribe(); diff --git a/src/plugins/console/public/application/containers/editor/editor.tsx b/src/plugins/console/public/application/containers/editor/editor.tsx index 0bfe837f2cd90..66d3cbab20ac5 100644 --- a/src/plugins/console/public/application/containers/editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/editor.tsx @@ -47,6 +47,7 @@ export const Editor = memo(({ loading }: Props) => { INITIAL_PANEL_WIDTH, ]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const onPanelWidthChange = useCallback( debounce((widths: number[]) => { storage.set(StorageKeys.WIDTH, widths); diff --git a/src/plugins/console/public/application/hooks/use_save_current_text_object.ts b/src/plugins/console/public/application/hooks/use_save_current_text_object.ts index ab517ba1bfdd1..1bd1a7fb09bd1 100644 --- a/src/plugins/console/public/application/hooks/use_save_current_text_object.ts +++ b/src/plugins/console/public/application/hooks/use_save_current_text_object.ts @@ -32,6 +32,7 @@ export const useSaveCurrentTextObject = () => { const { currentTextObject } = useEditorReadContext(); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ return useCallback( throttle( (text: string) => { diff --git a/src/plugins/console/server/lib/spec_definitions/js/filter.ts b/src/plugins/console/server/lib/spec_definitions/js/filter.ts index 27e02f7cf1837..b5e99e610b8ba 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/filter.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/filter.ts @@ -58,13 +58,6 @@ filters.limit = { value: 100, }; -filters.type = { - __template: { - value: 'TYPE', - }, - value: '{type}', -}; - filters.geo_bounding_box = { __template: { FIELD: { diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 8dd0a766da97b..a59d1e8c546d4 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -41,6 +41,7 @@ import { QueryState, SavedQuery, syncQueryStateWithUrl, + UI_SETTINGS, } from '../../../data/public'; import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../../saved_objects/public'; @@ -430,7 +431,8 @@ export class DashboardAppController { dashboardStateManager.getQuery() || { query: '', language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || + uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }, queryFilter.getFilters() ); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 7e25d80c9d619..5d4cc851cf455 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -186,7 +186,11 @@ export class DashboardContainer extends Container - + , dom diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx index 9b7cec2f182ba..9a2610a82b97d 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx @@ -80,6 +80,7 @@ function prepare(props?: Partial) { dashboardContainer = new DashboardContainer(initialInput, options); const defaultTestProps: DashboardGridProps = { container: dashboardContainer, + PanelComponent: () =>
, kibana: null as any, intl: null as any, }; diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index 19d9ad34e729c..dcd07fe394c7d 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; import ReactGridLayout, { Layout } from 'react-grid-layout'; import { GridData } from '../../../../common'; -import { ViewMode, EmbeddableChildPanel } from '../../../embeddable_plugin'; +import { ViewMode, EmbeddableChildPanel, EmbeddableStart } from '../../../embeddable_plugin'; import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; import { DashboardPanelState } from '../types'; import { withKibana } from '../../../../../kibana_react/public'; @@ -115,6 +115,7 @@ const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid); export interface DashboardGridProps extends ReactIntl.InjectedIntlProps { kibana: DashboardReactContextValue; + PanelComponent: EmbeddableStart['EmbeddablePanel']; container: DashboardContainer; } @@ -271,14 +272,7 @@ class DashboardGridUi extends React.Component {
); diff --git a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts index 62a39ee898d3a..1b060c186db97 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts @@ -108,16 +108,32 @@ interface IplacementDirection { fits: boolean; } +/** + * Compare grid data by an ending y coordinate. Grid data with a smaller ending y coordinate + * comes first. + * @param a + * @param b + */ +function comparePanels(a: GridData, b: GridData): number { + if (a.y + a.h < b.y + b.h) { + return -1; + } + if (a.y + a.h > b.y + b.h) { + return 1; + } + // a.y === b.y + if (a.x + a.w <= b.x + b.w) { + return -1; + } + return 1; +} + export function placePanelBeside({ width, height, currentPanels, placeBesideId, }: IPanelPlacementBesideArgs): Omit { - // const clonedPanels = _.cloneDeep(currentPanels); - if (!placeBesideId) { - throw new Error('Place beside method called without placeBesideId'); - } const panelToPlaceBeside = currentPanels[placeBesideId]; if (!panelToPlaceBeside) { throw new PanelNotFoundError(); @@ -130,10 +146,11 @@ export function placePanelBeside({ const possiblePlacementDirections: IplacementDirection[] = [ { grid: { x: beside.x + beside.w, y: beside.y, w: width, h: height }, fits: true }, // right - { grid: { x: beside.x - width, y: beside.y, w: width, h: height }, fits: true }, // left + { grid: { x: 0, y: beside.y + beside.h, w: width, h: height }, fits: true }, // left side of next row { grid: { x: beside.x, y: beside.y + beside.h, w: width, h: height }, fits: true }, // bottom ]; + // first, we check if there is place around the current panel for (const direction of possiblePlacementDirections) { if ( direction.grid.x >= 0 && @@ -156,13 +173,32 @@ export function placePanelBeside({ } } // if we get here that means there is no blank space around the panel we are placing beside. This means it's time to mess up the dashboard's groove. Fun! - const [, , bottomPlacement] = possiblePlacementDirections; - for (const currentPanelGrid of otherPanels) { - if (bottomPlacement.grid.y <= currentPanelGrid.y) { - const movedPanel = _.cloneDeep(currentPanels[currentPanelGrid.i]); - movedPanel.gridData.y = movedPanel.gridData.y + bottomPlacement.grid.h; - currentPanels[currentPanelGrid.i] = movedPanel; + /** + * 1. sort the panels in the grid + * 2. place the cloned panel to the bottom + * 3. reposition the panels after the cloned panel in the grid + */ + const grid = otherPanels.sort(comparePanels); + + let position = 0; + for (position; position < grid.length; position++) { + if (beside.i === grid[position].i) { + break; } } + const bottomPlacement = possiblePlacementDirections[2]; + // place to the bottom and move all other panels + let originalPositionInTheGrid = grid[position + 1].i; + const diff = + bottomPlacement.grid.y + + bottomPlacement.grid.h - + currentPanels[originalPositionInTheGrid].gridData.y; + + for (let j = position + 1; j < grid.length; j++) { + originalPositionInTheGrid = grid[j].i; + const movedPanel = _.cloneDeep(currentPanels[originalPositionInTheGrid]); + movedPanel.gridData.y = movedPanel.gridData.y + diff; + currentPanels[originalPositionInTheGrid] = movedPanel; + } return bottomPlacement.grid; } diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx index 1b257880b9401..25e451dc7f793 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx @@ -87,6 +87,7 @@ function getProps( dashboardContainer = new DashboardContainer(input, options); const defaultTestProps: DashboardViewportProps = { container: dashboardContainer, + PanelComponent: () =>
, }; return { diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index ae239bc27fdba..429837583b648 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; -import { PanelState } from '../../../embeddable_plugin'; +import { PanelState, EmbeddableStart } from '../../../embeddable_plugin'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; import { DashboardGrid } from '../grid'; import { context } from '../../../../../kibana_react/public'; @@ -27,6 +27,7 @@ import { context } from '../../../../../kibana_react/public'; export interface DashboardViewportProps { container: DashboardContainer; renderEmpty?: () => React.ReactNode; + PanelComponent: EmbeddableStart['EmbeddablePanel']; } interface State { @@ -114,7 +115,7 @@ export class DashboardViewport extends React.Component )} - +
); } diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 66a96e3e6e129..8ec72dc1f9a74 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -18,5 +18,33 @@ */ export const DEFAULT_QUERY_LANGUAGE = 'kuery'; -export const META_FIELDS_SETTING = 'metaFields'; -export const DOC_HIGHLIGHT_SETTING = 'doc_table:highlight'; + +export const UI_SETTINGS = { + META_FIELDS: 'metaFields', + DOC_HIGHLIGHT: 'doc_table:highlight', + QUERY_STRING_OPTIONS: 'query:queryString:options', + QUERY_ALLOW_LEADING_WILDCARDS: 'query:allowLeadingWildcards', + SEARCH_QUERY_LANGUAGE: 'search:queryLanguage', + SORT_OPTIONS: 'sort:options', + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: 'courier:ignoreFilterIfFieldNotInIndex', + COURIER_SET_REQUEST_PREFERENCE: 'courier:setRequestPreference', + COURIER_CUSTOM_REQUEST_PREFERENCE: 'courier:customRequestPreference', + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: 'courier:maxConcurrentShardRequests', + COURIER_BATCH_SEARCHES: 'courier:batchSearches', + SEARCH_INCLUDE_FROZEN: 'search:includeFrozen', + HISTOGRAM_BAR_TARGET: 'histogram:barTarget', + HISTOGRAM_MAX_BARS: 'histogram:maxBars', + HISTORY_LIMIT: 'history:limit', + SHORT_DOTS_ENABLE: 'shortDots:enable', + FORMAT_DEFAULT_TYPE_MAP: 'format:defaultTypeMap', + FORMAT_NUMBER_DEFAULT_PATTERN: 'format:number:defaultPattern', + FORMAT_PERCENT_DEFAULT_PATTERN: 'format:percent:defaultPattern', + FORMAT_BYTES_DEFAULT_PATTERN: 'format:bytes:defaultPattern', + FORMAT_CURRENCY_DEFAULT_PATTERN: 'format:currency:defaultPattern', + FORMAT_NUMBER_DEFAULT_LOCALE: 'format:number:defaultLocale', + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: 'timepicker:refreshIntervalDefaults', + TIMEPICKER_QUICK_RANGES: 'timepicker:quickRanges', + INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', + FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', + FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', +}; diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts b/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts index d146d81973d0d..5fa3c67dea400 100644 --- a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts +++ b/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts @@ -19,18 +19,19 @@ import { get } from 'lodash'; import { getEsQueryConfig } from './get_es_query_config'; import { IUiSettingsClient } from 'kibana/public'; +import { UI_SETTINGS } from '../../'; const config = ({ get(item: string) { return get(config, item); }, - 'query:allowLeadingWildcards': { + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: { allowLeadingWildcards: true, }, - 'query:queryString:options': { + [UI_SETTINGS.QUERY_STRING_OPTIONS]: { queryStringOptions: {}, }, - 'courier:ignoreFilterIfFieldNotInIndex': { + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: { ignoreFilterIfFieldNotInIndex: true, }, 'dateFormat:tz': { diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts b/src/plugins/data/common/es_query/es_query/get_es_query_config.ts index 0a82cf03bdb44..ff8fc5b11b26e 100644 --- a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts +++ b/src/plugins/data/common/es_query/es_query/get_es_query_config.ts @@ -18,15 +18,18 @@ */ import { EsQueryConfig } from './build_es_query'; +import { UI_SETTINGS } from '../../'; interface KibanaConfig { get(key: string): T; } export function getEsQueryConfig(config: KibanaConfig) { - const allowLeadingWildcards = config.get('query:allowLeadingWildcards'); - const queryStringOptions = config.get('query:queryString:options'); - const ignoreFilterIfFieldNotInIndex = config.get('courier:ignoreFilterIfFieldNotInIndex'); + const allowLeadingWildcards = config.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS); + const queryStringOptions = config.get(UI_SETTINGS.QUERY_STRING_OPTIONS); + const ignoreFilterIfFieldNotInIndex = config.get( + UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX + ); const dateFormatTZ = config.get('dateFormat:tz'); return { diff --git a/src/plugins/data/common/es_query/filters/build_filter.test.ts b/src/plugins/data/common/es_query/filters/build_filter.test.ts index 22b44035d6ca8..73a4a4c788863 100644 --- a/src/plugins/data/common/es_query/filters/build_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/build_filter.test.ts @@ -18,7 +18,7 @@ */ import { buildFilter, FilterStateStore, FILTERS } from '.'; -import { stubIndexPattern, stubFields } from '../../../public/stubs'; +import { stubIndexPattern, stubFields } from '../../../common/stubs'; describe('buildFilter', () => { it('should build phrase filters', () => { diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts b/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts index 2f31fafcb74e4..672c0a6db4dd0 100644 --- a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { stubIndexPattern, phraseFilter } from 'src/plugins/data/public/stubs'; +import { stubIndexPattern, phraseFilter } from 'src/plugins/data/common/stubs'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; describe('getIndexPatternFromFilter', () => { diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/src/plugins/data/common/es_query/kuery/ast/ast.ts index 01ce77fa8f578..c6f44ebc437d3 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.ts +++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts @@ -24,7 +24,7 @@ import { IIndexPattern } from '../../../index_patterns/types'; // @ts-ignore import { parse as parseKuery } from './_generated_/kuery'; -import { JsonObject } from '../../../../../kibana_utils/public'; +import { JsonObject } from '../../../../../kibana_utils/common'; const fromExpression = ( expression: string | DslQuery, diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts b/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts index 398cb1a164415..df1fdd1e0d514 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts @@ -21,7 +21,7 @@ import _ from 'lodash'; import * as ast from '../ast'; import { nodeTypes } from '../node_types'; import { NamedArgTypeBuildNode } from './types'; -import { JsonObject } from '../../../../../kibana_utils/public'; +import { JsonObject } from '../../../../../kibana_utils/common'; export function buildNode(name: string, value: any): NamedArgTypeBuildNode { const argumentNode = diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/src/plugins/data/common/es_query/kuery/node_types/types.ts index 937b5c6e7ef9c..6d3019e75d483 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/types.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/types.ts @@ -22,7 +22,7 @@ */ import { IIndexPattern } from '../../../index_patterns'; -import { JsonValue } from '../../../../../kibana_utils/public'; +import { JsonValue } from '../../../../../kibana_utils/common'; import { KueryNode } from '..'; export type FunctionName = diff --git a/src/plugins/data/common/field_formats/converters/bytes.test.ts b/src/plugins/data/common/field_formats/converters/bytes.test.ts index 8dad9fc206e72..e0c26170c2907 100644 --- a/src/plugins/data/common/field_formats/converters/bytes.test.ts +++ b/src/plugins/data/common/field_formats/converters/bytes.test.ts @@ -18,11 +18,12 @@ */ import { BytesFormat } from './bytes'; +import { UI_SETTINGS } from '../../constants'; describe('BytesFormat', () => { const config: Record = {}; - config['format:bytes:defaultPattern'] = '0,0.[000]b'; + config[UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN] = '0,0.[000]b'; const getConfig = (key: string) => config[key]; diff --git a/src/plugins/data/common/field_formats/converters/number.test.ts b/src/plugins/data/common/field_formats/converters/number.test.ts index fe36d5b12e873..31c5ea41bf5af 100644 --- a/src/plugins/data/common/field_formats/converters/number.test.ts +++ b/src/plugins/data/common/field_formats/converters/number.test.ts @@ -18,11 +18,12 @@ */ import { NumberFormat } from './number'; +import { UI_SETTINGS } from '../../constants'; describe('NumberFormat', () => { const config: Record = {}; - config['format:number:defaultPattern'] = '0,0.[000]'; + config[UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN] = '0,0.[000]'; const getConfig = (key: string) => config[key]; diff --git a/src/plugins/data/common/field_formats/converters/numeral.ts b/src/plugins/data/common/field_formats/converters/numeral.ts index a483b5a1e4f99..1d844bca3f89a 100644 --- a/src/plugins/data/common/field_formats/converters/numeral.ts +++ b/src/plugins/data/common/field_formats/converters/numeral.ts @@ -24,6 +24,7 @@ import numeralLanguages from '@elastic/numeral/languages'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert } from '../types'; +import { UI_SETTINGS } from '../../constants'; const numeralInst = numeral(); @@ -51,7 +52,8 @@ export abstract class NumeralFormat extends FieldFormat { if (isNaN(val)) return ''; const previousLocale = numeral.language(); - const defaultLocale = (this.getConfig && this.getConfig('format:number:defaultLocale')) || 'en'; + const defaultLocale = + (this.getConfig && this.getConfig(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE)) || 'en'; numeral.language(defaultLocale); const formatted = numeralInst.set(val).format(this.param('pattern')); diff --git a/src/plugins/data/common/field_formats/converters/percent.test.ts b/src/plugins/data/common/field_formats/converters/percent.test.ts index 8b26564814af3..754234bdeb78b 100644 --- a/src/plugins/data/common/field_formats/converters/percent.test.ts +++ b/src/plugins/data/common/field_formats/converters/percent.test.ts @@ -18,11 +18,12 @@ */ import { PercentFormat } from './percent'; +import { UI_SETTINGS } from '../../constants'; describe('PercentFormat', () => { const config: Record = {}; - config['format:percent:defaultPattern'] = '0,0.[000]%'; + config[UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN] = '0,0.[000]%'; const getConfig = (key: string) => config[key]; diff --git a/src/plugins/data/common/field_formats/converters/percent.ts b/src/plugins/data/common/field_formats/converters/percent.ts index ef3b0a1503a98..ecf9c7d19108d 100644 --- a/src/plugins/data/common/field_formats/converters/percent.ts +++ b/src/plugins/data/common/field_formats/converters/percent.ts @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { NumeralFormat } from './numeral'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; +import { UI_SETTINGS } from '../../constants'; export class PercentFormat extends NumeralFormat { static id = FIELD_FORMAT_IDS.PERCENT; @@ -32,7 +33,7 @@ export class PercentFormat extends NumeralFormat { allowsNumericalAggregations = true; getParamDefaults = () => ({ - pattern: this.getConfig!('format:percent:defaultPattern'), + pattern: this.getConfig!(UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN), fractional: true, }); diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.ts index 9e50d47bb2624..f00261e00971a 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.ts @@ -22,6 +22,7 @@ import { shortenDottedString } from '../../utils'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; +import { UI_SETTINGS } from '../../'; /** * Remove all of the whitespace between html tags @@ -71,7 +72,7 @@ export class SourceFormat extends FieldFormat { const formatted = field.indexPattern.formatHit(hit); const highlightPairs: any[] = []; const sourcePairs: any[] = []; - const isShortDots = this.getConfig!('shortDots:enable'); + const isShortDots = this.getConfig!(UI_SETTINGS.SHORT_DOTS_ENABLE); keys(formatted).forEach((key) => { const pairs = highlights[key] ? highlightPairs : sourcePairs; diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index c04a371066de3..9325485bce75d 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -33,6 +33,7 @@ import { baseFormatters } from './constants/base_formatters'; import { FieldFormat } from './field_format'; import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types'; +import { UI_SETTINGS } from '../'; export class FieldFormatsRegistry { protected fieldFormats: Map = new Map(); @@ -49,7 +50,7 @@ export class FieldFormatsRegistry { metaParamsOptions: Record = {}, defaultFieldConverters: FieldFormatInstanceType[] = baseFormatters ) { - const defaultTypeMap = getConfig('format:defaultTypeMap'); + const defaultTypeMap = getConfig(UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP); this.register(defaultFieldConverters); this.parseDefaultTypeMap(defaultTypeMap); this.getConfig = getConfig; diff --git a/src/plugins/data/public/index_patterns/field.stub.ts b/src/plugins/data/common/index_patterns/field.stub.ts similarity index 96% rename from src/plugins/data/public/index_patterns/field.stub.ts rename to src/plugins/data/common/index_patterns/field.stub.ts index 2e94f4b45f400..cbb3d2fa2ce68 100644 --- a/src/plugins/data/public/index_patterns/field.stub.ts +++ b/src/plugins/data/common/index_patterns/field.stub.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IFieldType } from '../../../../plugins/data/public'; +import { IFieldType } from '.'; export const stubFields: IFieldType[] = [ { diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/common/index_patterns/index_pattern.stub.ts similarity index 96% rename from src/plugins/data/public/index_patterns/index_pattern.stub.ts rename to src/plugins/data/common/index_patterns/index_pattern.stub.ts index 4f8108575aa15..e7384e09494aa 100644 --- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts +++ b/src/plugins/data/common/index_patterns/index_pattern.stub.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IIndexPattern } from '../../common'; +import { IIndexPattern } from '.'; import { stubFields } from './field.stub'; export const stubIndexPattern: IIndexPattern = { diff --git a/src/plugins/data/common/stubs.ts b/src/plugins/data/common/stubs.ts new file mode 100644 index 0000000000000..aea2b71eec46b --- /dev/null +++ b/src/plugins/data/common/stubs.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { stubIndexPattern, stubIndexPatternWithFields } from './index_patterns/index_pattern.stub'; +export { stubFields } from './index_patterns/field.stub'; +export * from './es_query/filters/stubs'; diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts new file mode 100644 index 0000000000000..09cb2cb2afeca --- /dev/null +++ b/src/plugins/data/config.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + autocomplete: schema.object({ + querySuggestions: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + valueSuggestions: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index cf248adea0b05..2136a405baad6 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -17,19 +17,30 @@ * under the License. */ -import { CoreSetup } from 'src/core/public'; +import { CoreSetup, PluginInitializerContext } from 'src/core/public'; import { QuerySuggestionGetFn } from './providers/query_suggestion_provider'; import { + getEmptyValueSuggestions, setupValueSuggestionProvider, ValueSuggestionsGetFn, } from './providers/value_suggestion_provider'; +import { ConfigSchema } from '../../config'; + export class AutocompleteService { + autocompleteConfig: ConfigSchema['autocomplete']; + + constructor(initializerContext: PluginInitializerContext) { + const { autocomplete } = initializerContext.config.get(); + + this.autocompleteConfig = autocomplete; + } + private readonly querySuggestionProviders: Map = new Map(); private getValueSuggestions?: ValueSuggestionsGetFn; private addQuerySuggestionProvider = (language: string, provider: QuerySuggestionGetFn): void => { - if (language && provider) { + if (language && provider && this.autocompleteConfig.querySuggestions.enabled) { this.querySuggestionProviders.set(language, provider); } }; @@ -47,7 +58,9 @@ export class AutocompleteService { /** @public **/ public setup(core: CoreSetup) { - this.getValueSuggestions = setupValueSuggestionProvider(core); + this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled + ? setupValueSuggestionProvider(core) + : getEmptyValueSuggestions; return { addQuerySuggestionProvider: this.addQuerySuggestionProvider, diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index 5df88000edbd5..a6a45a26f06b3 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -19,7 +19,7 @@ import { memoize } from 'lodash'; import { CoreSetup } from 'src/core/public'; -import { IIndexPattern, IFieldType } from '../../../common'; +import { IIndexPattern, IFieldType, UI_SETTINGS } from '../../../common'; function resolver(title: string, field: IFieldType, query: string, boolFilter: any) { // Only cache results for a minute @@ -38,6 +38,8 @@ interface ValueSuggestionsGetFnArgs { signal?: AbortSignal; } +export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSuggestionsGetFn; + export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => { const requestSuggestions = memoize( (index: string, field: IFieldType, query: string, boolFilter: any = [], signal?: AbortSignal) => @@ -56,7 +58,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG boolFilter, signal, }: ValueSuggestionsGetFnArgs): Promise => { - const shouldSuggestValues = core!.uiSettings.get('filterEditor:suggestValues'); + const shouldSuggestValues = core!.uiSettings.get( + UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES + ); const { title } = indexPattern; if (field.type === 'boolean') { diff --git a/src/plugins/data/public/field_formats/field_formats_service.ts b/src/plugins/data/public/field_formats/field_formats_service.ts index 22c7e90c06130..3ddc8d0b68a5b 100644 --- a/src/plugins/data/public/field_formats/field_formats_service.ts +++ b/src/plugins/data/public/field_formats/field_formats_service.ts @@ -18,7 +18,7 @@ */ import { CoreSetup } from 'src/core/public'; -import { FieldFormatsRegistry } from '../../common'; +import { FieldFormatsRegistry, UI_SETTINGS } from '../../common'; import { deserializeFieldFormat } from './utils/deserialize'; import { FormatFactory } from '../../common/field_formats/utils'; import { baseFormattersPublic } from './constants'; @@ -28,7 +28,7 @@ export class FieldFormatsService { public setup(core: CoreSetup) { core.uiSettings.getUpdate$().subscribe(({ key, newValue }) => { - if (key === 'format:defaultTypeMap') { + if (key === UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP) { this.fieldFormatsRegistry.parseDefaultTypeMap(newValue); } }); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 4a5b3fd5714db..5540039323756 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -17,9 +17,8 @@ * under the License. */ -import './index.scss'; - import { PluginInitializerContext } from '../../../core/public'; +import { ConfigSchema } from '../config'; /* * Filters: @@ -266,6 +265,7 @@ export { ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, + UI_SETTINGS, } from '../common'; /* @@ -450,7 +450,7 @@ export { import { DataPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { +export function plugin(initializerContext: PluginInitializerContext) { return new DataPublicPlugin(initializerContext); } diff --git a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx index 9115e523f5302..2088bd8c925df 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx +++ b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx @@ -91,9 +91,9 @@ export const createEnsureDefaultIndexPattern = (core: CoreStart) => { if (redirectTarget === '/home') { core.application.navigateToApp('home'); } else { - window.location.href = core.http.basePath.prepend( - `/app/kibana#/management/kibana/indexPatterns?bannerMessage=${bannerMessage}` - ); + core.application.navigateToApp('management', { + path: `/kibana/indexPatterns?bannerMessage=${bannerMessage}`, + }); } // return never-resolving promise to stop resolving and wait for the url change diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts index 43404c32cb3d4..3d54009d0fdca 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts @@ -33,7 +33,7 @@ import { KBN_FIELD_TYPES, IIndexPattern, IFieldType, - META_FIELDS_SETTING, + UI_SETTINGS, } from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; @@ -108,8 +108,8 @@ export class IndexPattern implements IIndexPattern { // which cause problems when being consumed from angular this.getConfig = getConfig; - this.shortDotsEnable = this.getConfig('shortDots:enable'); - this.metaFields = this.getConfig(META_FIELDS_SETTING); + this.shortDotsEnable = this.getConfig(UI_SETTINGS.SHORT_DOTS_ENABLE); + this.metaFields = this.getConfig(UI_SETTINGS.META_FIELDS); this.createFieldList = getIndexPatternFieldListCreator({ fieldFormats: getFieldFormats(), @@ -117,8 +117,12 @@ export class IndexPattern implements IIndexPattern { }); this.fields = this.createFieldList(this, [], this.shortDotsEnable); - this.fieldsFetcher = createFieldsFetcher(this, apiClient, this.getConfig(META_FIELDS_SETTING)); - this.flattenHit = flattenHitWrapper(this, this.getConfig(META_FIELDS_SETTING)); + this.fieldsFetcher = createFieldsFetcher( + this, + apiClient, + this.getConfig(UI_SETTINGS.META_FIELDS) + ); + this.flattenHit = flattenHitWrapper(this, this.getConfig(UI_SETTINGS.META_FIELDS)); this.formatHit = formatHitProvider( this, getFieldFormats().getDefaultInstance(KBN_FIELD_TYPES.STRING) diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 08796216db1e0..06b5cbdfdfdfb 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext, CoreSetup, @@ -24,6 +26,7 @@ import { Plugin, PackageInfo, } from 'src/core/public'; +import { ConfigSchema } from '../config'; import { Storage, IStorageWrapper, createStartServicesGetter } from '../../kibana_utils/public'; import { DataPublicPluginSetup, @@ -83,17 +86,18 @@ declare module '../../ui_actions/public' { } export class DataPublicPlugin implements Plugin { - private readonly autocomplete = new AutocompleteService(); + private readonly autocomplete: AutocompleteService; private readonly searchService: SearchService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; private readonly storage: IStorageWrapper; private readonly packageInfo: PackageInfo; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(); this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); + this.autocomplete = new AutocompleteService(initializerContext); this.storage = new Storage(window.localStorage); this.packageInfo = initializerContext.env.packageInfo; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 142ec9c8c877e..dcdb528ac8b7d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -53,6 +53,7 @@ import { SimpleSavedObject } from 'src/core/public'; import { Subscription } from 'rxjs'; import { Toast } from 'kibana/public'; import { ToastsStart } from 'kibana/public'; +import { TypeOf } from '@kbn/config-schema'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { Unit } from '@elastic/datemath'; @@ -1319,7 +1320,8 @@ export type PhrasesFilter = Filter & { // // @public (undocumented) export class Plugin implements Plugin_2 { - constructor(initializerContext: PluginInitializerContext_2); + // Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts + constructor(initializerContext: PluginInitializerContext_2); // Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1336,7 +1338,7 @@ export class Plugin implements Plugin_2): Plugin; // Warning: (ae-missing-release-tag) "Query" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1790,6 +1792,39 @@ export interface TimeRange { // @public export type TSearchStrategyProvider = (context: ISearchContext) => ISearchStrategy; +// Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +}; + // Warnings were encountered during analysis: // @@ -1798,38 +1833,38 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/common/es_query/filters/match_all_filter.ts:28:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts index 3c69a498e74cd..878142906f54b 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts @@ -24,14 +24,14 @@ import { Subscription } from 'rxjs'; import { FilterManager } from './filter_manager'; import { getFilter } from './test_helpers/get_stub_filter'; import { getFiltersArray } from './test_helpers/get_filters_array'; -import { Filter, FilterStateStore } from '../../../common'; +import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; const setupMock = coreMock.createSetup(); const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return pinnedByDefault; default: throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index d58a0eb45c04f..60a49a4bd50f4 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -34,6 +34,7 @@ import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS, + UI_SETTINGS, } from '../../../common'; export class FilterManager { @@ -129,7 +130,7 @@ export class FilterManager { public addFilters( filters: Filter[] | Filter, - pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault') + pinFilterStatus: boolean = this.uiSettings.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT) ) { if (!Array.isArray(filters)) { filters = [filters]; @@ -157,7 +158,7 @@ export class FilterManager { public setFilters( newFilters: Filter[], - pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault') + pinFilterStatus: boolean = this.uiSettings.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT) ) { const store = pinFilterStatus ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE; diff --git a/src/plugins/data/public/query/lib/get_query_log.ts b/src/plugins/data/public/query/lib/get_query_log.ts index a71eb7580cf07..b7827d2c8de02 100644 --- a/src/plugins/data/public/query/lib/get_query_log.ts +++ b/src/plugins/data/public/query/lib/get_query_log.ts @@ -20,6 +20,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { PersistedLog } from '../persisted_log'; +import { UI_SETTINGS } from '../../../common'; export function getQueryLog( uiSettings: IUiSettingsClient, @@ -30,7 +31,7 @@ export function getQueryLog( return new PersistedLog( `typeahead:${appName}-${language}`, { - maxLength: uiSettings.get('history:limit'), + maxLength: uiSettings.get(UI_SETTINGS.HISTORY_LIMIT), filterDuplicates: true, }, storage diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index 06e4c1c8be6d5..4e394445b75ae 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -20,7 +20,7 @@ import { Subscription } from 'rxjs'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; -import { Filter, FilterStateStore } from '../../../common'; +import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; import { BaseStateContainer, createStateContainer, Storage } from '../../../../kibana_utils/public'; import { QueryService, QueryStart } from '../query_service'; @@ -46,11 +46,11 @@ const startMock = coreMock.createStart(); setupMock.uiSettings.get.mockImplementation((key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return true; case 'timepicker:timeDefaults': return { from: 'now-15m', to: 'now' }; - case 'timepicker:refreshIntervalDefaults': + case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; default: throw new Error(`sync_query test: not mocked uiSetting: ${key}`); diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 50dc35ea955ee..7727153537257 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -21,7 +21,7 @@ import { Subscription } from 'rxjs'; import { createBrowserHistory, History } from 'history'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; -import { Filter, FilterStateStore } from '../../../common'; +import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; import { createKbnUrlStateStorage, @@ -39,11 +39,11 @@ const startMock = coreMock.createStart(); setupMock.uiSettings.get.mockImplementation((key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return true; case 'timepicker:timeDefaults': return { from: 'now-15m', to: 'now' }; - case 'timepicker:refreshIntervalDefaults': + case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; default: throw new Error(`sync_query test: not mocked uiSetting: ${key}`); diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index 413163ed059ad..df2fbc8e5a8f3 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -20,6 +20,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { TimeHistory, Timefilter, TimeHistoryContract, TimefilterContract } from './index'; +import { UI_SETTINGS } from '../../../common'; /** * Filter Service @@ -35,7 +36,7 @@ export class TimefilterService { public setup({ uiSettings, storage }: TimeFilterServiceDependencies): TimefilterSetup { const timefilterConfig = { timeDefaults: uiSettings.get('timepicker:timeDefaults'), - refreshIntervalDefaults: uiSettings.get('timepicker:refreshIntervalDefaults'), + refreshIntervalDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; const history = new TimeHistory(storage); const timefilter = new Timefilter(timefilterConfig, history); diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index d5c97d0c95c5c..8a5596f669cb7 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -31,7 +31,7 @@ import { dateHistogramInterval, TimeRange } from '../../../../common'; import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; -import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../common'; +import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; import { TimefilterContract } from '../../../query'; import { QuerySetup } from '../../../query/query_service'; import { GetInternalStartServicesFn } from '../../../types'; @@ -125,8 +125,8 @@ export const getDateHistogramBucketAgg = ({ const { timefilter } = query.timefilter; buckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index 3f3f13bb955c1..4052c0b390155 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -26,7 +26,7 @@ import { toAngularJSON } from '../utils'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { Storage } from '../../../../../../plugins/kibana_utils/public'; -import { getEsQueryConfig, buildEsQuery, Query } from '../../../../common'; +import { getEsQueryConfig, buildEsQuery, Query, UI_SETTINGS } from '../../../../common'; import { getQueryLog } from '../../../query'; import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; @@ -69,7 +69,10 @@ export const getFiltersBucketAgg = ({ { name: 'filters', default: [ - { input: { query: '', language: uiSettings.get('search:queryLanguage') }, label: '' }, + { + input: { query: '', language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) }, + label: '', + }, ], write(aggConfig, output) { const inFilters: FilterValue[] = aggConfig.params.filters; diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.ts b/src/plugins/data/public/search/aggs/buckets/histogram.ts index d04df4f8aac6b..c1fad17f488db 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.ts @@ -24,7 +24,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { KBN_FIELD_TYPES } from '../../../../common'; +import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; import { ExtendedBounds } from './lib/extended_bounds'; @@ -155,8 +155,8 @@ export const getHistogramBucketAgg = ({ const range = autoBounds.max - autoBounds.min; const bars = range / interval; - if (bars > uiSettings.get('histogram:maxBars')) { - const minInterval = range / uiSettings.get('histogram:maxBars'); + if (bars > uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { + const minInterval = range / uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); // Round interval by order of magnitude to provide clean intervals // Always round interval up so there will always be less buckets than histogram:maxBars diff --git a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts index 9d976784329cc..30fcdd9d83a38 100644 --- a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import { IUiSettingsClient } from 'src/core/public'; import { TimeBuckets } from '../buckets/lib/time_buckets'; -import { toAbsoluteDates, TimeRange } from '../../../../common'; +import { toAbsoluteDates, TimeRange, UI_SETTINGS } from '../../../../common'; export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { return function calculateAutoTimeExpression(range: TimeRange) { @@ -29,8 +29,8 @@ export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { } const buckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/src/plugins/data/public/search/es_search/get_es_preference.test.ts b/src/plugins/data/public/search/es_search/get_es_preference.test.ts index 8b8156b4519d6..05a74b3e6205a 100644 --- a/src/plugins/data/public/search/es_search/get_es_preference.test.ts +++ b/src/plugins/data/public/search/es_search/get_es_preference.test.ts @@ -20,6 +20,7 @@ import { getEsPreference } from './get_es_preference'; import { CoreStart } from '../../../../../core/public'; import { coreMock } from '../../../../../core/public/mocks'; +import { UI_SETTINGS } from '../../../common'; describe('Get ES preference', () => { let mockCoreStart: MockedKeys; @@ -30,8 +31,8 @@ describe('Get ES preference', () => { test('returns the session ID if set to sessionId', () => { mockCoreStart.uiSettings.get.mockImplementation((key: string) => { - if (key === 'courier:setRequestPreference') return 'sessionId'; - if (key === 'courier:customRequestPreference') return 'foobar'; + if (key === UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE) return 'sessionId'; + if (key === UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) return 'foobar'; }); const preference = getEsPreference(mockCoreStart.uiSettings, 'my_session_id'); expect(preference).toBe('my_session_id'); @@ -39,8 +40,8 @@ describe('Get ES preference', () => { test('returns the custom preference if set to custom', () => { mockCoreStart.uiSettings.get.mockImplementation((key: string) => { - if (key === 'courier:setRequestPreference') return 'custom'; - if (key === 'courier:customRequestPreference') return 'foobar'; + if (key === UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE) return 'custom'; + if (key === UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) return 'foobar'; }); const preference = getEsPreference(mockCoreStart.uiSettings); expect(preference).toBe('foobar'); @@ -48,8 +49,8 @@ describe('Get ES preference', () => { test('returns undefined if set to none', () => { mockCoreStart.uiSettings.get.mockImplementation((key: string) => { - if (key === 'courier:setRequestPreference') return 'none'; - if (key === 'courier:customRequestPreference') return 'foobar'; + if (key === UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE) return 'none'; + if (key === UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) return 'foobar'; }); const preference = getEsPreference(mockCoreStart.uiSettings); expect(preference).toBe(undefined); diff --git a/src/plugins/data/public/search/es_search/get_es_preference.ts b/src/plugins/data/public/search/es_search/get_es_preference.ts index 3f1c2b9b3b736..5e40712067bb0 100644 --- a/src/plugins/data/public/search/es_search/get_es_preference.ts +++ b/src/plugins/data/public/search/es_search/get_es_preference.ts @@ -18,12 +18,13 @@ */ import { IUiSettingsClient } from '../../../../../core/public'; +import { UI_SETTINGS } from '../../../common'; const defaultSessionId = `${Date.now()}`; export function getEsPreference(uiSettings: IUiSettingsClient, sessionId = defaultSessionId) { - const setPreference = uiSettings.get('courier:setRequestPreference'); + const setPreference = uiSettings.get(UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE); if (setPreference === 'sessionId') return `${sessionId}`; - const customPreference = uiSettings.get('courier:customRequestPreference'); + const customPreference = uiSettings.get(UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE); return setPreference === 'custom' ? customPreference : undefined; } diff --git a/src/plugins/data/public/search/fetch/get_search_params.test.ts b/src/plugins/data/public/search/fetch/get_search_params.test.ts index 4809d76a46f59..f9b62fdd4fc61 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.test.ts +++ b/src/plugins/data/public/search/fetch/get_search_params.test.ts @@ -19,6 +19,7 @@ import { getSearchParams } from './get_search_params'; import { IUiSettingsClient } from 'kibana/public'; +import { UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}) { return { @@ -40,21 +41,21 @@ describe('getSearchParams', () => { }); test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ 'search:includeFrozen': true }); + let config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true }); let searchParams = getSearchParams(config); expect(searchParams.ignore_throttled).toBe(false); - config = getConfigStub({ 'search:includeFrozen': false }); + config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false }); searchParams = getSearchParams(config); expect(searchParams.ignore_throttled).toBe(true); }); test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests', () => { - let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); + let config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 0 }); let searchParams = getSearchParams(config); expect(searchParams.max_concurrent_shard_requests).toBe(undefined); - config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); + config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5 }); searchParams = getSearchParams(config); expect(searchParams.max_concurrent_shard_requests).toBe(5); }); diff --git a/src/plugins/data/public/search/fetch/get_search_params.ts b/src/plugins/data/public/search/fetch/get_search_params.ts index f0c43bd2e74cd..60bdc9ed6473a 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.ts +++ b/src/plugins/data/public/search/fetch/get_search_params.ts @@ -18,6 +18,7 @@ */ import { IUiSettingsClient } from 'kibana/public'; +import { UI_SETTINGS } from '../../../common'; const sessionId = Date.now(); @@ -33,19 +34,19 @@ export function getSearchParams(config: IUiSettingsClient, esShardTimeout: numbe } export function getIgnoreThrottled(config: IUiSettingsClient) { - return !config.get('search:includeFrozen'); + return !config.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); } export function getMaxConcurrentShardRequests(config: IUiSettingsClient) { - const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); + const maxConcurrentShardRequests = config.get(UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS); return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; } export function getPreference(config: IUiSettingsClient) { - const setRequestPreference = config.get('courier:setRequestPreference'); + const setRequestPreference = config.get(UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE); if (setRequestPreference === 'sessionId') return sessionId; return setRequestPreference === 'custom' - ? config.get('courier:customRequestPreference') + ? config.get(UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) : undefined; } diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts index c619c9b17d9a8..436b522744622 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts @@ -21,6 +21,7 @@ import { IUiSettingsClient } from 'kibana/public'; import { defaultSearchStrategy } from './default_search_strategy'; import { searchStartMock } from '../mocks'; import { SearchStrategySearchParams } from './types'; +import { UI_SETTINGS } from '../../../common'; const { search } = defaultSearchStrategy; @@ -69,30 +70,30 @@ describe('defaultSearchStrategy', function () { }); test('does not send max_concurrent_shard_requests by default', async () => { - const config = getConfigStub({ 'courier:batchSearches': true }); + const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); }); test('allows configuration of max_concurrent_shard_requests', async () => { const config = getConfigStub({ - 'courier:batchSearches': true, - 'courier:maxConcurrentShardRequests': 42, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, + [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 42, }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); }); test('should set rest_total_hits_as_int to true on a request', async () => { - const config = getConfigStub({ 'courier:batchSearches': true }); + const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); }); test('should set ignore_throttled=false when including frozen indices', async () => { const config = getConfigStub({ - 'courier:batchSearches': true, - 'search:includeFrozen': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true, }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); @@ -100,7 +101,7 @@ describe('defaultSearchStrategy', function () { test('should properly call abort with msearch', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); search({ ...searchArgs, config }).abort(); expect(msearchMockResponse.abort).toHaveBeenCalled(); diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/public/search/legacy/fetch_soon.test.ts index e99e13ba33d1a..61d3568350b6b 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.test.ts @@ -22,6 +22,7 @@ import { callClient } from './call_client'; import { IUiSettingsClient } from 'kibana/public'; import { FetchHandlers, FetchOptions } from '../fetch/types'; import { SearchRequest, SearchResponse } from '../index'; +import { UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}) { return { @@ -60,7 +61,7 @@ describe('fetchSoon', () => { test('should execute asap if config is set to not batch searches', () => { const config = getConfigStub({ - 'courier:batchSearches': false, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: false, }); const request = {}; const options = {}; @@ -72,7 +73,7 @@ describe('fetchSoon', () => { test('should delay by 50ms if config is set to batch searches', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const request = {}; const options = {}; @@ -88,7 +89,7 @@ describe('fetchSoon', () => { test('should send a batch of requests to callClient', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const requests = [{ foo: 1 }, { foo: 2 }]; const options = [{ bar: 1 }, { bar: 2 }]; @@ -105,7 +106,7 @@ describe('fetchSoon', () => { test('should return the response to the corresponding call for multiple batched requests', async () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; @@ -120,7 +121,7 @@ describe('fetchSoon', () => { test('should wait for the previous batch to start before starting a new batch', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const firstBatch = [{ foo: 1 }, { foo: 2 }]; const secondBatch = [{ bar: 1 }, { bar: 2 }]; diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/public/search/legacy/fetch_soon.ts index 304c1c4d63f5b..fed2c52bc491f 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.ts @@ -20,6 +20,7 @@ import { callClient } from './call_client'; import { FetchHandlers, FetchOptions } from '../fetch/types'; import { SearchRequest, SearchResponse } from '../index'; +import { UI_SETTINGS } from '../../../common'; /** * This function introduces a slight delay in the request process to allow multiple requests to queue @@ -30,7 +31,7 @@ export async function fetchSoon( options: FetchOptions, fetchHandlers: FetchHandlers ) { - const msToDelay = fetchHandlers.config.get('courier:batchSearches') ? 50 : 0; + const msToDelay = fetchHandlers.config.get(UI_SETTINGS.COURIER_BATCH_SEARCHES) ? 50 : 0; return delayedFetch(request, options, fetchHandlers, msToDelay); } diff --git a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts b/src/plugins/data/public/search/legacy/get_msearch_params.test.ts index ce98f6ab2a7bb..dc61e19406631 100644 --- a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts +++ b/src/plugins/data/public/search/legacy/get_msearch_params.test.ts @@ -19,6 +19,7 @@ import { getMSearchParams } from './get_msearch_params'; import { IUiSettingsClient } from '../../../../../core/public'; +import { UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}) { return { @@ -34,29 +35,29 @@ describe('getMSearchParams', () => { }); test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ 'search:includeFrozen': true }); + let config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true }); let msearchParams = getMSearchParams(config); expect(msearchParams.ignore_throttled).toBe(false); - config = getConfigStub({ 'search:includeFrozen': false }); + config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false }); msearchParams = getMSearchParams(config); expect(msearchParams.ignore_throttled).toBe(true); }); test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests if greater than 0', () => { - let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); + let config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 0 }); let msearchParams = getMSearchParams(config); expect(msearchParams.max_concurrent_shard_requests).toBe(undefined); - config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); + config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5 }); msearchParams = getMSearchParams(config); expect(msearchParams.max_concurrent_shard_requests).toBe(5); }); test('does not include other search params that are included in the msearch header or body', () => { const config = getConfigStub({ - 'search:includeFrozen': false, - 'courier:maxConcurrentShardRequests': 5, + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5, }); const msearchParams = getMSearchParams(config); expect(msearchParams.hasOwnProperty('ignore_unavailable')).toBe(false); diff --git a/src/plugins/data/public/search/long_query_notification.tsx b/src/plugins/data/public/search/long_query_notification.tsx index 0bdf8ab7c66f8..1db298618fae8 100644 --- a/src/plugins/data/public/search/long_query_notification.tsx +++ b/src/plugins/data/public/search/long_query_notification.tsx @@ -44,7 +44,7 @@ export function LongQueryNotification(props: Props) { { - await props.application.navigateToApp('kibana#/management/stack/license_management'); + await props.application.navigateToApp('management/stack/license_management'); }} > filter(docvalueField.field) @@ -448,7 +447,7 @@ export class SearchSource { body.query = buildEsQuery(index, query, filters, esQueryConfigs); if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, uiSettings.get(DOC_HIGHLIGHT_SETTING)); + body.highlight = getHighlightRequest(body.query, uiSettings.get(UI_SETTINGS.DOC_HIGHLIGHT)); delete searchRequest.highlightAll; } diff --git a/src/plugins/data/public/stubs.ts b/src/plugins/data/public/stubs.ts index d2519716dd83e..1b44d35addb66 100644 --- a/src/plugins/data/public/stubs.ts +++ b/src/plugins/data/public/stubs.ts @@ -17,6 +17,4 @@ * under the License. */ -export { stubIndexPattern, stubIndexPatternWithFields } from './index_patterns/index_pattern.stub'; -export { stubFields } from './index_patterns/field.stub'; -export * from '../common/es_query/filters/stubs'; +export * from '../common/stubs'; diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index a54a25acc5913..43dba150bf8d4 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -36,6 +36,7 @@ import { toggleFilterDisabled, toggleFilterNegated, unpinFilter, + UI_SETTINGS, } from '../../../common'; interface Props { @@ -76,7 +77,7 @@ function FilterBarUI(props: Props) { } function renderAddFilter() { - const isPinned = uiSettings!.get('filters:pinnedByDefault'); + const isPinned = uiSettings!.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT); const [indexPattern] = props.indexPatterns; const index = indexPattern && indexPattern.id; const newFilter = buildEmptyFilter(isPinned, index); diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 546365b89d9be..94138f60b52b1 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -22,6 +22,7 @@ import { debounce } from 'lodash'; import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; import { IDataPluginServices, IIndexPattern, IFieldType } from '../../..'; +import { UI_SETTINGS } from '../../../../common'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; @@ -54,7 +55,9 @@ export class PhraseSuggestorUI extends React.Com } protected isSuggestingValues() { - const shouldSuggestValues = this.services.uiSettings.get('filterEditor:suggestValues'); + const shouldSuggestValues = this.services.uiSettings.get( + UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES + ); const { field } = this.props; return shouldSuggestValues && field && field.aggregatable && field.type === 'string'; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index c44e1faeb8e7f..053fca7d5773b 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -74,7 +74,8 @@ export function FilterItem(props: Props) { setIndexPatternExists(false); }); } else { - setIndexPatternExists(false); + // Allow filters without an index pattern and don't validate them. + setIndexPatternExists(true); } }, [props.filter.meta.index]); @@ -244,6 +245,9 @@ export function FilterItem(props: Props) { * This function makes this behavior explicit, but it needs to be revised. */ function isFilterApplicable() { + // Any filter is applicable if no index patterns were provided to FilterBar. + if (!props.indexPatterns.length) return true; + const ip = getIndexPatternFromFilter(filter, indexPatterns); if (ip) return true; diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index f579adbc0c7e2..5f2d4c00cd6b6 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -28,6 +28,7 @@ import { dataPluginMock } from '../../mocks'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; import { stubIndexPatternWithFields } from '../../stubs'; +import { UI_SETTINGS } from '../../../common'; const startMock = coreMock.createStart(); const mockTimeHistory = { @@ -38,7 +39,7 @@ const mockTimeHistory = { startMock.uiSettings.get.mockImplementation((key: string) => { switch (key) { - case 'timepicker:quickRanges': + case UI_SETTINGS.TIMEPICKER_QUICK_RANGES: return [ { from: 'now/d', @@ -48,7 +49,7 @@ startMock.uiSettings.get.mockImplementation((key: string) => { ]; case 'dateFormat': return 'MMM D, YYYY @ HH:mm:ss.SSS'; - case 'history:limit': + case UI_SETTINGS.HISTORY_LIMIT: return 10; case 'timepicker:timeDefaults': return { diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 433cb652ee5ce..f65bf97e391e2 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -38,7 +38,7 @@ import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { useKibana, toMountPoint } from '../../../../kibana_react/public'; import { QueryStringInput } from './query_string_input'; -import { doesKueryExpressionHaveLuceneSyntaxError } from '../../../common'; +import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; interface Props { @@ -255,7 +255,7 @@ export function QueryBarTopRow(props: Props) { } const commonlyUsedRanges = uiSettings! - .get('timepicker:quickRanges') + .get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES) .map(({ from, to, display }: { from: string; to: string; display: string }) => { return { start: from, diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 7723254f3aa51..81e84e3198072 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -27,7 +27,7 @@ import { useFilterManager } from './lib/use_filter_manager'; import { useTimefilter } from './lib/use_timefilter'; import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; -import { Filter, Query, TimeRange } from '../../../common'; +import { Filter, Query, TimeRange, UI_SETTINGS } from '../../../common'; interface StatefulSearchBarDeps { core: CoreStart; @@ -125,7 +125,8 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) const defaultQuery = { query: '', language: - storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'), + storage.get('kibana.userQueryLanguage') || + core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }; const [query, setQuery] = useState(props.query || defaultQuery); @@ -134,12 +135,14 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) queryRef.current = props.query; setQuery(props.query || defaultQuery); } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [defaultQuery, props.query]); useEffect(() => { if (props.onQuerySubmit !== onQuerySubmitRef.current) { onQuerySubmitRef.current = props.onQuerySubmit; } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [props.onQuerySubmit]); // handle service state updates. diff --git a/src/plugins/data/server/autocomplete/autocomplete_service.ts b/src/plugins/data/server/autocomplete/autocomplete_service.ts index 412e9b6236195..07f0ce0e7c1e5 100644 --- a/src/plugins/data/server/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/server/autocomplete/autocomplete_service.ts @@ -17,14 +17,23 @@ * under the License. */ +import { TypeOf } from '@kbn/config-schema'; import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { registerRoutes } from './routes'; +import { ConfigSchema, configSchema } from '../../config'; export class AutocompleteService implements Plugin { - constructor(private initializerContext: PluginInitializerContext) {} + private valueSuggestionsEnabled: boolean = true; + + constructor(private initializerContext: PluginInitializerContext) { + initializerContext.config.create>().subscribe((configUpdate) => { + this.valueSuggestionsEnabled = configUpdate.autocomplete.valueSuggestions.enabled; + }); + } public setup(core: CoreSetup) { - registerRoutes(core, this.initializerContext.config.legacy.globalConfig$); + if (this.valueSuggestionsEnabled) + registerRoutes(core, this.initializerContext.config.legacy.globalConfig$); } public start() {} diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 47bef4255347c..831d23864d228 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -17,7 +17,8 @@ * under the License. */ -import { PluginInitializerContext } from '../../../core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; +import { ConfigSchema, configSchema } from '../config'; import { DataServerPlugin, DataPluginSetup, DataPluginStart } from './plugin'; import { @@ -145,6 +146,7 @@ export { ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, + UI_SETTINGS, } from '../common'; /** @@ -213,7 +215,7 @@ export { * @public */ -export function plugin(initializerContext: PluginInitializerContext) { +export function plugin(initializerContext: PluginInitializerContext) { return new DataServerPlugin(initializerContext); } @@ -222,3 +224,10 @@ export { DataPluginSetup as PluginSetup, DataPluginStart as PluginStart, }; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + autocomplete: true, + }, + schema: configSchema, +}; diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts index 446320b09757a..81e352fea51b2 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts @@ -22,6 +22,9 @@ import { APICaller } from 'kibana/server'; jest.mock('../../../common', () => ({ DEFAULT_QUERY_LANGUAGE: 'lucene', + UI_SETTINGS: { + SEARCH_QUERY_LANGUAGE: 'search:queryLanguage', + }, })); let fetch: ReturnType; diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts index 9f3437161541f..157716b38f523 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts @@ -19,7 +19,7 @@ import { get } from 'lodash'; import { APICaller } from 'kibana/server'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../common'; +import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../../../common'; const defaultSearchQueryLanguageSetting = DEFAULT_QUERY_LANGUAGE; @@ -40,7 +40,7 @@ export function fetchProvider(index: string) { const queryLanguageConfigValue = get( config, - 'hits.hits[0]._source.config.search:queryLanguage' + `hits.hits[0]._source.config.${UI_SETTINGS.SEARCH_QUERY_LANGUAGE}` ); // search:queryLanguage can potentially be in four states in the .kibana index: diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 83a5358642ce4..8c9d0df2ed894 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -18,6 +18,7 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/server'; +import { ConfigSchema } from '../config'; import { IndexPatternsService } from './index_patterns'; import { ISearchSetup } from './search'; import { SearchService } from './search/search_service'; @@ -27,7 +28,7 @@ import { KqlTelemetryService } from './kql_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; import { AutocompleteService } from './autocomplete'; import { FieldFormatsService, FieldFormatsSetup, FieldFormatsStart } from './field_formats'; -import { uiSettings } from './ui_settings'; +import { getUiSettings } from './ui_settings'; export interface DataPluginSetup { search: ISearchSetup; @@ -51,7 +52,7 @@ export class DataServerPlugin implements Plugin) { this.searchService = new SearchService(initializerContext); this.scriptsService = new ScriptsService(); this.kqlTelemetryService = new KqlTelemetryService(initializerContext); @@ -64,7 +65,8 @@ export class DataServerPlugin implements Plugin KBN_FIELD_TYPES; +// Warning: (ae-forgotten-export) The symbol "PluginConfigDescriptor" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "config" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const config: PluginConfigDescriptor; + // @public (undocumented) export enum ES_FIELD_TYPES { // (undocumented) @@ -604,7 +611,7 @@ export function parseInterval(interval: string): moment.Duration | null; // @public (undocumented) export class Plugin implements Plugin_2 { // Warning: (ae-forgotten-export) The symbol "PluginInitializerContext" needs to be exported by the entry point index.d.ts - constructor(initializerContext: PluginInitializerContext); + constructor(initializerContext: PluginInitializerContext); // Warning: (ae-forgotten-export) The symbol "DataPluginSetupDependencies" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -627,7 +634,7 @@ export class Plugin implements Plugin_2 { } // @public -export function plugin(initializerContext: PluginInitializerContext): Plugin; +export function plugin(initializerContext: PluginInitializerContext): Plugin; // Warning: (ae-missing-release-tag) "DataPluginSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -713,38 +720,71 @@ export interface TimeRange { // @public export type TSearchStrategyProvider = (context: ISearchContext, caller: APICaller_2, search: ISearchGeneric) => ISearchStrategy; +// Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +}; + // Warnings were encountered during analysis: // -// src/plugins/data/server/index.ts:39:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:39:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:70:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:70:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:130:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:130:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:182:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:189:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/plugin.ts:65:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:191:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/plugin.ts:66:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 5af62be295201..de978c7968aee 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -19,33 +19,652 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; - import { UiSettingsParams } from 'kibana/server'; -import { META_FIELDS_SETTING, DOC_HIGHLIGHT_SETTING } from '../common'; +// @ts-ignore untyped module +import numeralLanguages from '@elastic/numeral/languages'; +import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../common'; + +const luceneQueryLanguageLabel = i18n.translate('data.advancedSettings.searchQueryLanguageLucene', { + defaultMessage: 'Lucene', +}); + +const queryLanguageSettingName = i18n.translate('data.advancedSettings.searchQueryLanguageTitle', { + defaultMessage: 'Query language', +}); -export const uiSettings: Record = { - [META_FIELDS_SETTING]: { - name: i18n.translate('data.advancedSettings.metaFieldsTitle', { - defaultMessage: 'Meta fields', - }), - value: ['_source', '_id', '_type', '_index', '_score'], - description: i18n.translate('data.advancedSettings.metaFieldsText', { - defaultMessage: - 'Fields that exist outside of _source to merge into our document when displaying it', - }), - schema: schema.arrayOf(schema.string()), - }, - [DOC_HIGHLIGHT_SETTING]: { - name: i18n.translate('data.advancedSettings.docTableHighlightTitle', { - defaultMessage: 'Highlight results', - }), - value: true, - description: i18n.translate('data.advancedSettings.docTableHighlightText', { - defaultMessage: - 'Highlight results in Discover and Saved Searches Dashboard. ' + - 'Highlighting makes requests slow when working on big documents.', - }), - category: ['discover'], - schema: schema.boolean(), - }, +const requestPreferenceOptionLabels = { + sessionId: i18n.translate('data.advancedSettings.courier.requestPreferenceSessionId', { + defaultMessage: 'Session ID', + }), + custom: i18n.translate('data.advancedSettings.courier.requestPreferenceCustom', { + defaultMessage: 'Custom', + }), + none: i18n.translate('data.advancedSettings.courier.requestPreferenceNone', { + defaultMessage: 'None', + }), }; + +// We add the `en` key manually here, since that's not a real numeral locale, but the +// default fallback in case the locale is not found. +const numeralLanguageIds = [ + 'en', + ...numeralLanguages.map((numeralLanguage: any) => { + return numeralLanguage.id; + }), +]; + +export function getUiSettings(): Record> { + return { + [UI_SETTINGS.META_FIELDS]: { + name: i18n.translate('data.advancedSettings.metaFieldsTitle', { + defaultMessage: 'Meta fields', + }), + value: ['_source', '_id', '_type', '_index', '_score'], + description: i18n.translate('data.advancedSettings.metaFieldsText', { + defaultMessage: + 'Fields that exist outside of _source to merge into our document when displaying it', + }), + schema: schema.arrayOf(schema.string()), + }, + [UI_SETTINGS.DOC_HIGHLIGHT]: { + name: i18n.translate('data.advancedSettings.docTableHighlightTitle', { + defaultMessage: 'Highlight results', + }), + value: true, + description: i18n.translate('data.advancedSettings.docTableHighlightText', { + defaultMessage: + 'Highlight results in Discover and Saved Searches Dashboard. ' + + 'Highlighting makes requests slow when working on big documents.', + }), + category: ['discover'], + schema: schema.boolean(), + }, + [UI_SETTINGS.QUERY_STRING_OPTIONS]: { + name: i18n.translate('data.advancedSettings.query.queryStringOptionsTitle', { + defaultMessage: 'Query string options', + }), + value: '{ "analyze_wildcard": true }', + description: i18n.translate('data.advancedSettings.query.queryStringOptionsText', { + defaultMessage: + '{optionsLink} for the lucene query string parser. Is only used when "{queryLanguage}" is set ' + + 'to {luceneLanguage}.', + description: + 'Part of composite text: data.advancedSettings.query.queryStringOptions.optionsLinkText + ' + + 'data.advancedSettings.query.queryStringOptionsText', + values: { + optionsLink: + '' + + i18n.translate('data.advancedSettings.query.queryStringOptions.optionsLinkText', { + defaultMessage: 'Options', + }) + + '', + luceneLanguage: luceneQueryLanguageLabel, + queryLanguage: queryLanguageSettingName, + }, + }), + type: 'json', + schema: schema.object({ + analyze_wildcard: schema.boolean(), + }), + }, + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: { + name: i18n.translate('data.advancedSettings.query.allowWildcardsTitle', { + defaultMessage: 'Allow leading wildcards in query', + }), + value: true, + description: i18n.translate('data.advancedSettings.query.allowWildcardsText', { + defaultMessage: + 'When set, * is allowed as the first character in a query clause. ' + + 'Currently only applies when experimental query features are enabled in the query bar. ' + + 'To disallow leading wildcards in basic lucene queries, use {queryStringOptionsPattern}.', + values: { + queryStringOptionsPattern: UI_SETTINGS.QUERY_STRING_OPTIONS, + }, + }), + schema: schema.boolean(), + }, + [UI_SETTINGS.SEARCH_QUERY_LANGUAGE]: { + name: queryLanguageSettingName, + value: DEFAULT_QUERY_LANGUAGE, + description: i18n.translate('data.advancedSettings.searchQueryLanguageText', { + defaultMessage: + 'Query language used by the query bar. KQL is a new language built specifically for Kibana.', + }), + type: 'select', + options: ['lucene', 'kuery'], + optionLabels: { + lucene: luceneQueryLanguageLabel, + kuery: i18n.translate('data.advancedSettings.searchQueryLanguageKql', { + defaultMessage: 'KQL', + }), + }, + schema: schema.string(), + }, + [UI_SETTINGS.SORT_OPTIONS]: { + name: i18n.translate('data.advancedSettings.sortOptionsTitle', { + defaultMessage: 'Sort options', + }), + value: '{ "unmapped_type": "boolean" }', + description: i18n.translate('data.advancedSettings.sortOptionsText', { + defaultMessage: '{optionsLink} for the Elasticsearch sort parameter', + description: + 'Part of composite text: data.advancedSettings.sortOptions.optionsLinkText + ' + + 'data.advancedSettings.sortOptionsText', + values: { + optionsLink: + '' + + i18n.translate('data.advancedSettings.sortOptions.optionsLinkText', { + defaultMessage: 'Options', + }) + + '', + }, + }), + type: 'json', + schema: schema.object({ + unmapped_type: schema.string(), + }), + }, + defaultIndex: { + name: i18n.translate('data.advancedSettings.defaultIndexTitle', { + defaultMessage: 'Default index', + }), + value: null, + type: 'string', + description: i18n.translate('data.advancedSettings.defaultIndexText', { + defaultMessage: 'The index to access if no index is set', + }), + schema: schema.nullable(schema.string()), + }, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: { + name: i18n.translate('data.advancedSettings.courier.ignoreFilterTitle', { + defaultMessage: 'Ignore filter(s)', + }), + value: false, + description: i18n.translate('data.advancedSettings.courier.ignoreFilterText', { + defaultMessage: + 'This configuration enhances support for dashboards containing visualizations accessing dissimilar indexes. ' + + 'When disabled, all filters are applied to all visualizations. ' + + 'When enabled, filter(s) will be ignored for a visualization ' + + `when the visualization's index does not contain the filtering field.`, + }), + category: ['search'], + schema: schema.boolean(), + }, + [UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE]: { + name: i18n.translate('data.advancedSettings.courier.requestPreferenceTitle', { + defaultMessage: 'Request preference', + }), + value: 'sessionId', + options: ['sessionId', 'custom', 'none'], + optionLabels: requestPreferenceOptionLabels, + type: 'select', + description: i18n.translate('data.advancedSettings.courier.requestPreferenceText', { + defaultMessage: `Allows you to set which shards handle your search requests. +
    +
  • {sessionId}: restricts operations to execute all search requests on the same shards. + This has the benefit of reusing shard caches across requests.
  • +
  • {custom}: allows you to define a your own preference. + Use 'courier:customRequestPreference' to customize your preference value.
  • +
  • {none}: means do not set a preference. + This might provide better performance because requests can be spread across all shard copies. + However, results might be inconsistent because different shards might be in different refresh states.
  • +
`, + values: { + sessionId: requestPreferenceOptionLabels.sessionId, + custom: requestPreferenceOptionLabels.custom, + none: requestPreferenceOptionLabels.none, + }, + }), + category: ['search'], + schema: schema.string(), + }, + [UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE]: { + name: i18n.translate('data.advancedSettings.courier.customRequestPreferenceTitle', { + defaultMessage: 'Custom request preference', + }), + value: '_local', + type: 'string', + description: i18n.translate('data.advancedSettings.courier.customRequestPreferenceText', { + defaultMessage: + '{requestPreferenceLink} used when {setRequestReferenceSetting} is set to {customSettingValue}.', + description: + 'Part of composite text: data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText + ' + + 'data.advancedSettings.courier.customRequestPreferenceText', + values: { + setRequestReferenceSetting: `${UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE}`, + customSettingValue: '"custom"', + requestPreferenceLink: + '' + + i18n.translate( + 'data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText', + { + defaultMessage: 'Request Preference', + } + ) + + '', + }, + }), + category: ['search'], + schema: schema.string(), + }, + [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: { + name: i18n.translate('data.advancedSettings.courier.maxRequestsTitle', { + defaultMessage: 'Max Concurrent Shard Requests', + }), + value: 0, + type: 'number', + description: i18n.translate('data.advancedSettings.courier.maxRequestsText', { + defaultMessage: + 'Controls the {maxRequestsLink} setting used for _msearch requests sent by Kibana. ' + + 'Set to 0 to disable this config and use the Elasticsearch default.', + values: { + maxRequestsLink: `max_concurrent_shard_requests`, + }, + }), + category: ['search'], + schema: schema.number(), + }, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: { + name: i18n.translate('data.advancedSettings.courier.batchSearchesTitle', { + defaultMessage: 'Batch concurrent searches', + }), + value: false, + type: 'boolean', + description: i18n.translate('data.advancedSettings.courier.batchSearchesText', { + defaultMessage: `When disabled, dashboard panels will load individually, and search requests will terminate when users navigate + away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, and + searches will not terminate.`, + }), + deprecation: { + message: i18n.translate('data.advancedSettings.courier.batchSearchesTextDeprecation', { + defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', + }), + docLinksKey: 'kibanaSearchSettings', + }, + category: ['search'], + schema: schema.boolean(), + }, + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: { + name: 'Search in frozen indices', + description: `Will include frozen indices in results if enabled. Searching through frozen indices + might increase the search time.`, + value: false, + category: ['search'], + schema: schema.boolean(), + }, + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: { + name: i18n.translate('data.advancedSettings.histogram.barTargetTitle', { + defaultMessage: 'Target bars', + }), + value: 50, + description: i18n.translate('data.advancedSettings.histogram.barTargetText', { + defaultMessage: + 'Attempt to generate around this many bars when using "auto" interval in date histograms', + }), + schema: schema.number(), + }, + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: { + name: i18n.translate('data.advancedSettings.histogram.maxBarsTitle', { + defaultMessage: 'Maximum bars', + }), + value: 100, + description: i18n.translate('data.advancedSettings.histogram.maxBarsText', { + defaultMessage: + 'Never show more than this many bars in date histograms, scale values if needed', + }), + schema: schema.number(), + }, + [UI_SETTINGS.HISTORY_LIMIT]: { + name: i18n.translate('data.advancedSettings.historyLimitTitle', { + defaultMessage: 'History limit', + }), + value: 10, + description: i18n.translate('data.advancedSettings.historyLimitText', { + defaultMessage: + 'In fields that have history (e.g. query inputs), show this many recent values', + }), + schema: schema.number(), + }, + [UI_SETTINGS.SHORT_DOTS_ENABLE]: { + name: i18n.translate('data.advancedSettings.shortenFieldsTitle', { + defaultMessage: 'Shorten fields', + }), + value: false, + description: i18n.translate('data.advancedSettings.shortenFieldsText', { + defaultMessage: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz', + }), + schema: schema.boolean(), + }, + [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: { + name: i18n.translate('data.advancedSettings.format.defaultTypeMapTitle', { + defaultMessage: 'Field type format name', + }), + value: `{ + "ip": { "id": "ip", "params": {} }, + "date": { "id": "date", "params": {} }, + "date_nanos": { "id": "date_nanos", "params": {}, "es": true }, + "number": { "id": "number", "params": {} }, + "boolean": { "id": "boolean", "params": {} }, + "_source": { "id": "_source", "params": {} }, + "_default_": { "id": "string", "params": {} } +}`, + type: 'json', + description: i18n.translate('data.advancedSettings.format.defaultTypeMapText', { + defaultMessage: + 'Map of the format name to use by default for each field type. ' + + '{defaultFormat} is used if the field type is not mentioned explicitly', + values: { + defaultFormat: '"_default_"', + }, + }), + schema: schema.object({ + ip: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + date: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + date_nanos: schema.object({ + id: schema.string(), + params: schema.object({}), + es: schema.boolean(), + }), + number: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + boolean: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + _source: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + _default_: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + }), + }, + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.numberFormatTitle', { + defaultMessage: 'Number format', + }), + value: '0,0.[000]', + type: 'string', + description: i18n.translate('data.advancedSettings.format.numberFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "number" format', + description: + 'Part of composite text: data.advancedSettings.format.numberFormatText + ' + + 'data.advancedSettings.format.numberFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.numberFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.percentFormatTitle', { + defaultMessage: 'Percent format', + }), + value: '0,0.[000]%', + type: 'string', + description: i18n.translate('data.advancedSettings.format.percentFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "percent" format', + description: + 'Part of composite text: data.advancedSettings.format.percentFormatText + ' + + 'data.advancedSettings.format.percentFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.percentFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.bytesFormatTitle', { + defaultMessage: 'Bytes format', + }), + value: '0,0.[0]b', + type: 'string', + description: i18n.translate('data.advancedSettings.format.bytesFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "bytes" format', + description: + 'Part of composite text: data.advancedSettings.format.bytesFormatText + ' + + 'data.advancedSettings.format.bytesFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.bytesFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.currencyFormatTitle', { + defaultMessage: 'Currency format', + }), + value: '($0,0.[00])', + type: 'string', + description: i18n.translate('data.advancedSettings.format.currencyFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "currency" format', + description: + 'Part of composite text: data.advancedSettings.format.currencyFormatText + ' + + 'data.advancedSettings.format.currencyFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.currencyFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: { + name: i18n.translate('data.advancedSettings.format.formattingLocaleTitle', { + defaultMessage: 'Formatting locale', + }), + value: 'en', + type: 'select', + options: numeralLanguageIds, + optionLabels: Object.fromEntries( + numeralLanguages.map((language: Record) => [language.id, language.name]) + ), + description: i18n.translate('data.advancedSettings.format.formattingLocaleText', { + defaultMessage: `{numeralLanguageLink} locale`, + description: + 'Part of composite text: data.advancedSettings.format.formattingLocale.numeralLanguageLinkText + ' + + 'data.advancedSettings.format.formattingLocaleText', + values: { + numeralLanguageLink: + '' + + i18n.translate( + 'data.advancedSettings.format.formattingLocale.numeralLanguageLinkText', + { + defaultMessage: 'Numeral language', + } + ) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + name: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { + defaultMessage: 'Time filter refresh interval', + }), + value: `{ + "pause": false, + "value": 0 +}`, + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsText', { + defaultMessage: `The timefilter's default refresh interval`, + }), + requiresPageReload: true, + schema: schema.object({ + pause: schema.boolean(), + value: schema.number(), + }), + }, + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: { + name: i18n.translate('data.advancedSettings.timepicker.quickRangesTitle', { + defaultMessage: 'Time filter quick ranges', + }), + value: JSON.stringify( + [ + { + from: 'now/d', + to: 'now/d', + display: i18n.translate('data.advancedSettings.timepicker.today', { + defaultMessage: 'Today', + }), + }, + { + from: 'now/w', + to: 'now/w', + display: i18n.translate('data.advancedSettings.timepicker.thisWeek', { + defaultMessage: 'This week', + }), + }, + { + from: 'now-15m', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last15Minutes', { + defaultMessage: 'Last 15 minutes', + }), + }, + { + from: 'now-30m', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last30Minutes', { + defaultMessage: 'Last 30 minutes', + }), + }, + { + from: 'now-1h', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last1Hour', { + defaultMessage: 'Last 1 hour', + }), + }, + { + from: 'now-24h', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last24Hours', { + defaultMessage: 'Last 24 hours', + }), + }, + { + from: 'now-7d', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last7Days', { + defaultMessage: 'Last 7 days', + }), + }, + { + from: 'now-30d', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last30Days', { + defaultMessage: 'Last 30 days', + }), + }, + { + from: 'now-90d', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last90Days', { + defaultMessage: 'Last 90 days', + }), + }, + { + from: 'now-1y', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last1Year', { + defaultMessage: 'Last 1 year', + }), + }, + ], + null, + 2 + ), + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.quickRangesText', { + defaultMessage: + 'The list of ranges to show in the Quick section of the time filter. This should be an array of objects, ' + + 'with each object containing "from", "to" (see {acceptedFormatsLink}), and ' + + '"display" (the title to be displayed).', + description: + 'Part of composite text: data.advancedSettings.timepicker.quickRangesText + ' + + 'data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', + values: { + acceptedFormatsLink: + `` + + i18n.translate('data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', { + defaultMessage: 'accepted formats', + }) + + '', + }, + }), + schema: schema.arrayOf( + schema.object({ + from: schema.string(), + to: schema.string(), + display: schema.string(), + }) + ), + }, + [UI_SETTINGS.INDEXPATTERN_PLACEHOLDER]: { + name: i18n.translate('data.advancedSettings.indexPatternPlaceholderTitle', { + defaultMessage: 'Index pattern placeholder', + }), + value: '', + description: i18n.translate('data.advancedSettings.indexPatternPlaceholderText', { + defaultMessage: + 'The placeholder for the "Index pattern name" field in "Management > Index Patterns > Create Index Pattern".', + }), + schema: schema.string(), + }, + [UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT]: { + name: i18n.translate('data.advancedSettings.pinFiltersTitle', { + defaultMessage: 'Pin filters by default', + }), + value: false, + description: i18n.translate('data.advancedSettings.pinFiltersText', { + defaultMessage: 'Whether the filters should have a global state (be pinned) by default', + }), + schema: schema.boolean(), + }, + [UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES]: { + name: i18n.translate('data.advancedSettings.suggestFilterValuesTitle', { + defaultMessage: 'Filter editor suggest values', + description: '"Filter editor" refers to the UI you create filters in.', + }), + value: true, + description: i18n.translate('data.advancedSettings.suggestFilterValuesText', { + defaultMessage: + 'Set this property to false to prevent the filter editor from suggesting values for fields.', + }), + schema: schema.boolean(), + }, + }; +} diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 0b3a07e98624e..14dd399697b56 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -1,6 +1,7 @@ { "id": "discover", "version": "kibana", + "optionalPlugins": ["share"], "server": true, "ui": true, "requiredPlugins": [ diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index f9ccdbf5ab535..88885b3eb211b 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -72,6 +72,7 @@ import { syncQueryStateWithUrl, getDefaultQuery, search, + UI_SETTINGS, } from '../../../../data/public'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../kibana_legacy/public'; @@ -162,8 +163,8 @@ app.config(($routeProvider) => { mapping: { search: '/', 'index-pattern': { - app: 'kibana', - path: `#/management/kibana/objects/savedSearches/${$route.current.params.id}`, + app: 'management', + path: `kibana/objects/savedSearches/${$route.current.params.id}`, }, }, toastNotifications, @@ -592,7 +593,8 @@ function discoverController( const query = $scope.searchSource.getField('query') || getDefaultQuery( - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage') + localStorage.get('kibana.userQueryLanguage') || + config.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) ); return { query, @@ -750,6 +752,13 @@ function discoverController( // Update defaults so that "reload saved query" functions correctly setAppState(getStateDefaults()); chrome.docTitle.change(savedSearch.lastSavedTitle); + chrome.setBreadcrumbs([ + { + text: discoverBreadcrumbsTitle, + href: '#/', + }, + { text: savedSearch.title }, + ]); } } }); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts index 60dfb69e85e74..82bfcc8bc42f9 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts @@ -19,6 +19,7 @@ import { TableHeader } from './table_header/table_header'; import { getServices } from '../../../../kibana_services'; import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; +import { UI_SETTINGS } from '../../../../../../data/public'; export function createTableHeaderDirective(reactDirective: any) { const { uiSettings: config } = getServices(); @@ -38,7 +39,7 @@ export function createTableHeaderDirective(reactDirective: any) { { restrict: 'A' }, { hideTimeColumn: config.get(DOC_HIDE_TIME_COLUMN_SETTING, false), - isShortDots: config.get('shortDots:enable'), + isShortDots: config.get(UI_SETTINGS.SHORT_DOTS_ENABLE), defaultSortOrder: config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), } ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 99a5547ed0760..5a319d30b2515 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -33,6 +33,7 @@ import { IIndexPatternFieldList, IndexPatternField, IndexPattern, + UI_SETTINGS, } from '../../../../../data/public'; import { AppState } from '../../angular/discover_state'; import { getDetails } from './lib/get_details'; @@ -133,7 +134,7 @@ export function DiscoverSidebar({ ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); - const useShortDots = services.uiSettings.get('shortDots:enable'); + const useShortDots = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const { selected: selectedFields, diff --git a/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap index 3204252c808ac..42cd8613b1de0 100644 --- a/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap +++ b/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap @@ -53,7 +53,7 @@ exports[`render 1`] = ` > { return { getServices: () => ({ core: { uiSettings: {}, savedObjects: {} }, + addBasePath: (path) => path, }), }; }); diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 359d91325f064..4154fdfeb3ff4 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -27,3 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; +export { DISCOVER_APP_URL_GENERATOR } from './url_generator'; diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index c394fe2c11a71..e4314426bfce5 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -34,6 +34,9 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { savedSearchLoader: {} as any, + urlGenerator: { + createUrl: jest.fn(), + } as any, }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 4323e3d8deda4..091288e3e65aa 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -34,7 +34,7 @@ import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; -import { SharePluginStart } from 'src/plugins/share/public'; +import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public'; import { VisualizationsStart, VisualizationsSetup } from 'src/plugins/visualizations/public'; import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; @@ -43,7 +43,7 @@ import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../d import { SavedObjectLoader } from '../../saved_objects/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; - +import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; import { DocViewTable } from './application/components/table/table'; @@ -59,6 +59,17 @@ import { import { createSavedSearchesLoader } from './saved_searches'; import { registerFeature } from './register_feature'; import { buildServices } from './build_services'; +import { + DiscoverUrlGeneratorState, + DISCOVER_APP_URL_GENERATOR, + DiscoverUrlGenerator, +} from './url_generator'; + +declare module '../../share/public' { + export interface UrlGeneratorStateMapping { + [DISCOVER_APP_URL_GENERATOR]: UrlGeneratorState; + } +} /** * @public @@ -76,12 +87,31 @@ export interface DiscoverSetup { export interface DiscoverStart { savedSearchLoader: SavedObjectLoader; + + /** + * `share` plugin URL generator for Discover app. Use it to generate links into + * Discover application, example: + * + * ```ts + * const url = await plugins.discover.urlGenerator.createUrl({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + */ + readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; } /** * @internal */ export interface DiscoverSetupPlugins { + share?: SharePluginSetup; uiActions: UiActionsSetup; embeddable: EmbeddableSetup; kibanaLegacy: KibanaLegacySetup; @@ -122,6 +152,7 @@ export class DiscoverPlugin private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; private innerAngularInitialized: boolean = false; + private urlGenerator?: DiscoverStart['urlGenerator']; /** * why are those functions public? they are needed for some mocha tests @@ -131,6 +162,17 @@ export class DiscoverPlugin public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { + const baseUrl = core.http.basePath.prepend('/app/discover'); + + if (plugins.share) { + this.urlGenerator = plugins.share.urlGenerators.registerUrlGenerator( + new DiscoverUrlGenerator({ + appBasePath: baseUrl, + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + } + this.docViewsRegistry = new DocViewsRegistry(); setDocViewsRegistry(this.docViewsRegistry); this.docViewsRegistry.addDocView({ @@ -158,7 +200,7 @@ export class DiscoverPlugin // so history is lazily created (when app is mounted) // this prevents redundant `#` when not in discover app getHistory: getScopedHistory, - baseUrl: core.http.basePath.prepend('/app/discover'), + baseUrl, defaultSubUrl: '#/', storageKey: `lastUrl:${core.http.basePath.get()}:discover`, navLinkUpdater$: this.appStateUpdater, @@ -266,6 +308,7 @@ export class DiscoverPlugin }; return { + urlGenerator: this.urlGenerator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts new file mode 100644 index 0000000000000..cf9beb246fea2 --- /dev/null +++ b/src/plugins/discover/public/url_generator.test.ts @@ -0,0 +1,259 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DiscoverUrlGenerator } from './url_generator'; +import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public'; +// eslint-disable-next-line +import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; +import { FilterStateStore } from '../../data/common'; + +const appBasePath: string = 'xyz/app/discover'; +const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; +const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; + +interface SetupParams { + useHash?: boolean; +} + +const setup = async ({ useHash = false }: SetupParams = {}) => { + const generator = new DiscoverUrlGenerator({ + appBasePath, + useHash, + }); + + return { + generator, + }; +}; + +beforeEach(() => { + // @ts-ignore + hashedItemStore.storage = mockStorage; +}); + +describe('Discover url generator', () => { + test('can create a link to Discover with no state and no saved search', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({}); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(url.startsWith(appBasePath)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can create a link to a saved search in Discover', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ savedSearchId }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(url.startsWith(`${appBasePath}#/${savedSearchId}`)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can specify specific index pattern', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + indexPatternId, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + index: indexPatternId, + }); + expect(_g).toEqual({}); + }); + + test('can specify specific time range', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }); + }); + + test('can specify query', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + expect(_g).toEqual({}); + }); + + test('can specify local and global filters', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + filters: [ + { + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + { + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + }, + ], + }); + expect(_g).toEqual({ + filters: [ + { + $state: { + store: 'globalState', + }, + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + }, + ], + }); + }); + + test('can set refresh interval', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + }); + + test('can set time range', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + timeRange: { + from: 'now-3h', + to: 'now', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-3h', + to: 'now', + }, + }); + }); + + describe('useHash property', () => { + describe('when default useHash is set to false', () => { + test('when using default, sets index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(true); + }); + + test('when enabling useHash, does not set index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + useHash: true, + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(false); + }); + }); + + describe('when default useHash is set to true', () => { + test('when using default, does not set index pattern ID in the generated URL', async () => { + const { generator } = await setup({ useHash: true }); + const url = await generator.createUrl({ + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(false); + }); + + test('when disabling useHash, sets index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + useHash: false, + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(true); + }); + }); + }); +}); diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts new file mode 100644 index 0000000000000..42d689050d5ad --- /dev/null +++ b/src/plugins/discover/public/url_generator.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + TimeRange, + Filter, + Query, + esFilters, + QueryState, + RefreshInterval, +} from '../../data/public'; +import { setStateToKbnUrl } from '../../kibana_utils/public'; +import { UrlGeneratorsDefinition } from '../../share/public'; + +export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR'; + +export interface DiscoverUrlGeneratorState { + /** + * Optionally set saved search ID. + */ + savedSearchId?: string; + + /** + * Optionally set index pattern ID. + */ + indexPatternId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval; + + /** + * Optionally apply filers. + */ + filters?: Filter[]; + + /** + * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the + * saved dashboard has a query saved with it, this will _replace_ that query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; +} + +interface Params { + appBasePath: string; + useHash: boolean; +} + +export class DiscoverUrlGenerator + implements UrlGeneratorsDefinition { + constructor(private readonly params: Params) {} + + public readonly id = DISCOVER_APP_URL_GENERATOR; + + public readonly createUrl = async ({ + filters, + indexPatternId, + query, + refreshInterval, + savedSearchId, + timeRange, + useHash = this.params.useHash, + }: DiscoverUrlGeneratorState): Promise => { + const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : ''; + const appState: { + query?: Query; + filters?: Filter[]; + index?: string; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (indexPatternId) appState.index = indexPatternId; + + if (timeRange) queryState.time = timeRange; + if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let url = `${this.params.appBasePath}#/${savedSearchPath}`; + url = setStateToKbnUrl('_g', queryState, { useHash }, url); + url = setStateToKbnUrl('_a', appState, { useHash }, url); + + return url; + }; +} diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx index b046376a304ae..e29e941e898fb 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx @@ -31,7 +31,7 @@ import { // eslint-disable-next-line import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { mount } from 'enzyme'; -import { embeddablePluginMock } from '../../mocks'; +import { embeddablePluginMock, createEmbeddablePanelMock } from '../../mocks'; test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => { const inspector = inspectorPluginMock.createStartContract(); @@ -58,18 +58,17 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async expect(newEmbeddable.id).toBeDefined(); + const testPanel = createEmbeddablePanelMock({ + getAllEmbeddableFactories: start.getEmbeddableFactories, + getEmbeddableFactory, + inspector, + }); + const component = mount( Promise.resolve([])} - getAllEmbeddableFactories={start.getEmbeddableFactories} - getEmbeddableFactory={getEmbeddableFactory} - notifications={{} as any} - application={{} as any} - overlays={{} as any} - inspector={inspector} - SavedObjectFinder={() => null} + PanelComponent={testPanel} /> ); @@ -97,19 +96,9 @@ test(`EmbeddableChildPanel renders an error message if the factory doesn't exist { getEmbeddableFactory } as any ); + const testPanel = createEmbeddablePanelMock({ inspector }); const component = mount( - Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} - notifications={{} as any} - overlays={{} as any} - application={{} as any} - inspector={inspector} - SavedObjectFinder={() => null} - /> + ); await nextTick(); diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx index 70628665e6e8c..be8ff2c95fe09 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx @@ -22,12 +22,7 @@ import React from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import { Subscription } from 'rxjs'; -import { CoreStart } from 'src/core/public'; -import { UiActionsService } from 'src/plugins/ui_actions/public'; - -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { ErrorEmbeddable, IEmbeddable } from '../embeddables'; -import { EmbeddablePanel } from '../panel'; import { IContainer } from './i_container'; import { EmbeddableStart } from '../../plugin'; @@ -35,14 +30,7 @@ export interface EmbeddableChildPanelProps { embeddableId: string; className?: string; container: IContainer; - getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - notifications: CoreStart['notifications']; - application: CoreStart['application']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + PanelComponent: EmbeddableStart['EmbeddablePanel']; } interface State { @@ -87,6 +75,7 @@ export class EmbeddableChildPanel extends React.Component ) : ( - + )} ); diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index 31e14a0af59d7..913c3a0b30826 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -19,9 +19,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'src/core/public'; -import { UiActionsService } from 'src/plugins/ui_actions/public'; -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { Container, ViewMode, ContainerInput } from '../..'; import { HelloWorldContainerComponent } from './hello_world_container_component'; import { EmbeddableStart } from '../../../plugin'; @@ -45,14 +42,8 @@ interface HelloWorldContainerInput extends ContainerInput { } interface HelloWorldContainerOptions { - getActions: UiActionsService['getTriggerCompatibleActions']; getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - application: CoreStart['application']; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + panelComponent: EmbeddableStart['EmbeddablePanel']; } export class HelloWorldContainer extends Container { @@ -78,14 +69,7 @@ export class HelloWorldContainer extends Container , node diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx index 6453046b86e20..5fefa1fc90720 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx @@ -20,22 +20,12 @@ import React, { Component, RefObject } from 'react'; import { Subscription } from 'rxjs'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { CoreStart } from 'src/core/public'; -import { UiActionsService } from 'src/plugins/ui_actions/public'; -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { IContainer, PanelState, EmbeddableChildPanel } from '../..'; import { EmbeddableStart } from '../../../plugin'; interface Props { container: IContainer; - getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - application: CoreStart['application']; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + panelComponent: EmbeddableStart['EmbeddablePanel']; } interface State { @@ -108,14 +98,7 @@ export class HelloWorldContainerComponent extends Component { ); diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts deleted file mode 100644 index f5487c381cfcb..0000000000000 --- a/src/plugins/embeddable/public/mocks.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { - EmbeddableStart, - EmbeddableSetup, - EmbeddableSetupDependencies, - EmbeddableStartDependencies, -} from '.'; -import { EmbeddablePublicPlugin } from './plugin'; -import { coreMock } from '../../../core/public/mocks'; - -// eslint-disable-next-line -import { inspectorPluginMock } from '../../inspector/public/mocks'; -// eslint-disable-next-line -import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; - -export type Setup = jest.Mocked; -export type Start = jest.Mocked; - -const createSetupContract = (): Setup => { - const setupContract: Setup = { - registerEmbeddableFactory: jest.fn(), - setCustomEmbeddableFactoryProvider: jest.fn(), - }; - return setupContract; -}; - -const createStartContract = (): Start => { - const startContract: Start = { - getEmbeddableFactories: jest.fn(), - getEmbeddableFactory: jest.fn(), - EmbeddablePanel: jest.fn(), - }; - return startContract; -}; - -const createInstance = (setupPlugins: Partial = {}) => { - const plugin = new EmbeddablePublicPlugin({} as any); - const setup = plugin.setup(coreMock.createSetup(), { - uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(), - }); - const doStart = (startPlugins: Partial = {}) => - plugin.start(coreMock.createStart(), { - uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(), - inspector: inspectorPluginMock.createStartContract(), - }); - return { - plugin, - setup, - doStart, - }; -}; - -export const embeddablePluginMock = { - createSetupContract, - createStartContract, - createInstance, -}; diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx new file mode 100644 index 0000000000000..9da0b7602c4f8 --- /dev/null +++ b/src/plugins/embeddable/public/mocks.tsx @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { + EmbeddableStart, + EmbeddableSetup, + EmbeddableSetupDependencies, + EmbeddableStartDependencies, + IEmbeddable, + EmbeddablePanel, +} from '.'; +import { EmbeddablePublicPlugin } from './plugin'; +import { coreMock } from '../../../core/public/mocks'; +import { UiActionsService } from './lib/ui_actions'; +import { CoreStart } from '../../../core/public'; +import { Start as InspectorStart } from '../../inspector/public'; + +// eslint-disable-next-line +import { inspectorPluginMock } from '../../inspector/public/mocks'; +// eslint-disable-next-line +import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +interface CreateEmbeddablePanelMockArgs { + getActions: UiActionsService['getTriggerCompatibleActions']; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + overlays: CoreStart['overlays']; + notifications: CoreStart['notifications']; + application: CoreStart['application']; + inspector: InspectorStart; + SavedObjectFinder: React.ComponentType; +} + +export const createEmbeddablePanelMock = ({ + getActions, + getEmbeddableFactory, + getAllEmbeddableFactories, + overlays, + notifications, + application, + inspector, + SavedObjectFinder, +}: Partial) => { + return ({ embeddable }: { embeddable: IEmbeddable }) => ( + Promise.resolve([]))} + getAllEmbeddableFactories={getAllEmbeddableFactories || ((() => []) as any)} + getEmbeddableFactory={getEmbeddableFactory || ((() => undefined) as any)} + notifications={notifications || ({} as any)} + application={application || ({} as any)} + overlays={overlays || ({} as any)} + inspector={inspector || ({} as any)} + SavedObjectFinder={SavedObjectFinder || (() => null)} + /> + ); +}; + +const createSetupContract = (): Setup => { + const setupContract: Setup = { + registerEmbeddableFactory: jest.fn(), + setCustomEmbeddableFactoryProvider: jest.fn(), + }; + return setupContract; +}; + +const createStartContract = (): Start => { + const startContract: Start = { + getEmbeddableFactories: jest.fn(), + getEmbeddableFactory: jest.fn(), + EmbeddablePanel: jest.fn(), + }; + return startContract; +}; + +const createInstance = (setupPlugins: Partial = {}) => { + const plugin = new EmbeddablePublicPlugin({} as any); + const setup = plugin.setup(coreMock.createSetup(), { + uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(), + }); + const doStart = (startPlugins: Partial = {}) => + plugin.start(coreMock.createStart(), { + uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), + }); + return { + plugin, + setup, + doStart, + }; +}; + +export const embeddablePluginMock = { + createSetupContract, + createStartContract, + createInstance, +}; diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index ebb76c743393b..ec92f334267f5 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -31,8 +31,7 @@ import { FilterableEmbeddableInput, } from '../lib/test_samples'; // eslint-disable-next-line -import { inspectorPluginMock } from '../../../../plugins/inspector/public/mocks'; -import { esFilters } from '../../../../plugins/data/public'; +import { esFilters } from '../../../data/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { const { doStart, setup } = testPlugin(); @@ -95,26 +94,16 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a }); test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { - const { doStart, coreStart, setup } = testPlugin(); - const inspector = inspectorPluginMock.createStartContract(); + const { doStart, setup } = testPlugin(); const factory = new FilterableEmbeddableFactory(); setup.registerEmbeddableFactory(factory.type, factory); const api = doStart(); const applyFilterAction = createFilterAction(); - const parent = new HelloWorldContainer( - { id: 'root', panels: {} }, - { - getActions: () => Promise.resolve([]), - getEmbeddableFactory: api.getEmbeddableFactory, - getAllEmbeddableFactories: api.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector, - SavedObjectFinder: () => null, - } - ); + + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { + getEmbeddableFactory: api.getEmbeddableFactory, + } as any); const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, EmbeddableOutput, @@ -130,27 +119,17 @@ test('ApplyFilterAction is incompatible if the root container does not accept a }); test('trying to execute on incompatible context throws an error ', async () => { - const { doStart, coreStart, setup } = testPlugin(); - const inspector = inspectorPluginMock.createStartContract(); + const { doStart, setup } = testPlugin(); const factory = new FilterableEmbeddableFactory(); setup.registerEmbeddableFactory(factory.type, factory); const api = doStart(); const applyFilterAction = createFilterAction(); - const parent = new HelloWorldContainer( - { id: 'root', panels: {} }, - { - getActions: () => Promise.resolve([]), - getEmbeddableFactory: api.getEmbeddableFactory, - getAllEmbeddableFactories: api.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector, - SavedObjectFinder: () => null, - } - ); + + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { + getEmbeddableFactory: api.getEmbeddableFactory, + } as any); const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index 4cd01abaf7995..490f0c00c7c4d 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -48,6 +48,7 @@ import { coreMock } from '../../../../core/public/mocks'; import { testPlugin } from './test_plugin'; import { of } from './helpers'; import { esFilters, Filter } from '../../../../plugins/data/public'; +import { createEmbeddablePanelMock } from '../mocks'; async function creatHelloWorldContainerAndEmbeddable( containerInput: ContainerInput = { id: 'hello', panels: {} }, @@ -68,15 +69,18 @@ async function creatHelloWorldContainerAndEmbeddable( const start = doStart(); - const container = new HelloWorldContainer(containerInput, { + const testPanel = createEmbeddablePanelMock({ getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + }); + + const container = new HelloWorldContainer(containerInput, { + getEmbeddableFactory: start.getEmbeddableFactory, + panelComponent: testPanel, }); const embeddable = await container.addNewEmbeddable< ContactCardEmbeddableInput, @@ -88,7 +92,7 @@ async function creatHelloWorldContainerAndEmbeddable( throw new Error('Error adding embeddable'); } - return { container, embeddable, coreSetup, coreStart, setup, start, uiActions }; + return { container, embeddable, coreSetup, coreStart, setup, start, uiActions, testPanel }; } test('Container initializes embeddables', async (done) => { @@ -131,7 +135,8 @@ test('Container.addNewEmbeddable', async () => { }); test('Container.removeEmbeddable removes and cleans up', async (done) => { - const { start, coreStart, uiActions } = await creatHelloWorldContainerAndEmbeddable(); + const { start, testPanel } = await creatHelloWorldContainerAndEmbeddable(); + const container = new HelloWorldContainer( { id: 'hello', @@ -143,14 +148,8 @@ test('Container.removeEmbeddable removes and cleans up', async (done) => { }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); const embeddable = await container.addNewEmbeddable< @@ -323,15 +322,17 @@ test(`Container updates its state when a child's input is updated`, async (done) // Make sure a brand new container built off the output of container also creates an embeddable // with "Dr.", not the default the embeddable was first added with. Makes sure changed input // is preserved with the container. - const containerClone = new HelloWorldContainer(container.getInput(), { + const testPanel = createEmbeddablePanelMock({ getActions: uiActions.getTriggerCompatibleActions, - getAllEmbeddableFactories: start.getEmbeddableFactories, getEmbeddableFactory: start.getEmbeddableFactory, - notifications: coreStart.notifications, + getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, + notifications: coreStart.notifications, application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + }); + const containerClone = new HelloWorldContainer(container.getInput(), { + getEmbeddableFactory: start.getEmbeddableFactory, + panelComponent: testPanel, }); const cloneSubscription = Rx.merge( containerClone.getOutput$(), @@ -575,6 +576,14 @@ test('Container changes made directly after adding a new embeddable are propagat const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -582,14 +591,8 @@ test('Container changes made directly after adding a new embeddable are propagat viewMode: ViewMode.EDIT, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -701,20 +704,22 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in coreMock.createStart() ); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', panels: {}, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -731,6 +736,14 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy const factory = new HelloWorldEmbeddableFactory(); setup.registerEmbeddableFactory(factory.type, factory); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -742,14 +755,8 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -771,6 +778,14 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem setup.registerEmbeddableFactory(factory.type, factory); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -782,14 +797,8 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -812,6 +821,14 @@ test('adding a panel then subsequently removing it before its loaded removes the }); setup.registerEmbeddableFactory(factory.type, factory); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -823,14 +840,8 @@ test('adding a panel then subsequently removing it before its loaded removes the }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx index a9cb83504d958..311efae49f735 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -37,6 +37,7 @@ import { testPlugin } from './test_plugin'; import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal'; import { mount } from 'enzyme'; import { EmbeddableStart } from '../plugin'; +import { createEmbeddablePanelMock } from '../mocks'; let api: EmbeddableStart; let container: Container; @@ -55,17 +56,20 @@ beforeEach(async () => { setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); api = doStart(); + + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: api.getEmbeddableFactory, + getAllEmbeddableFactories: api.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); container = new HelloWorldContainer( { id: '123', panels: {} }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: api.getEmbeddableFactory, - getAllEmbeddableFactories: api.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); const contactCardEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/tests/explicit_input.test.ts b/src/plugins/embeddable/public/tests/explicit_input.test.ts index 6bea4fe46a497..d64ff94d71800 100644 --- a/src/plugins/embeddable/public/tests/explicit_input.test.ts +++ b/src/plugins/embeddable/public/tests/explicit_input.test.ts @@ -36,6 +36,7 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world // eslint-disable-next-line import { coreMock } from '../../../../core/public/mocks'; import { esFilters, Filter } from '../../../../plugins/data/public'; +import { createEmbeddablePanelMock } from '../mocks'; const { setup, doStart, coreStart, uiActions } = testPlugin( coreMock.createSetup(), @@ -80,17 +81,19 @@ test('Explicit embeddable input mapped to undefined will default to inherited', }); test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async (done) => { + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', panels: {} }, { - getActions: uiActions.getTriggerCompatibleActions, - getAllEmbeddableFactories: start.getEmbeddableFactories, getEmbeddableFactory: start.getEmbeddableFactory, - notifications: coreStart.notifications, - overlays: coreStart.overlays, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -121,6 +124,14 @@ test('Explicit embeddable input mapped to undefined with no inherited value will // but before the embeddable factory returns the embeddable, that the `inheritedChildInput` and // embeddable input comparisons won't cause explicit input to be set when it shouldn't. test('Explicit input tests in async situations', (done: () => void) => { + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -132,14 +143,8 @@ test('Explicit input tests in async situations', (done: () => void) => { }, }, { - getActions: uiActions.getTriggerCompatibleActions, - getAllEmbeddableFactories: start.getEmbeddableFactories, getEmbeddableFactory: start.getEmbeddableFactory, - notifications: coreStart.notifications, - overlays: coreStart.overlays, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/monaco/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/monaco/index.ts new file mode 100644 index 0000000000000..a9c6ea1e01d54 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/monaco/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useXJsonMode } from './use_xjson_mode'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/monaco/use_xjson_mode.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/monaco/use_xjson_mode.ts new file mode 100644 index 0000000000000..b783045492f05 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/monaco/use_xjson_mode.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { XJsonLang } from '@kbn/monaco'; +import { useXJsonMode as useBaseXJsonMode } from '../xjson'; + +interface ReturnValue extends ReturnType { + XJsonLang: typeof XJsonLang; +} + +export const useXJsonMode = (json: Parameters[0]): ReturnValue => { + return { + ...useBaseXJsonMode(json), + XJsonLang, + }; +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/index.ts new file mode 100644 index 0000000000000..a9c6ea1e01d54 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useXJsonMode } from './use_xjson_mode'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/use_xjson_mode.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/use_xjson_mode.ts new file mode 100644 index 0000000000000..7dcc7c9ed83bc --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/xjson/use_xjson_mode.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, Dispatch } from 'react'; +import { collapseLiteralStrings, expandLiteralStrings } from '../../public'; + +interface ReturnValue { + xJson: string; + setXJson: Dispatch; + convertToJson: typeof collapseLiteralStrings; +} + +export const useXJsonMode = (json: Record | string | null): ReturnValue => { + const [xJson, setXJson] = useState(() => + json === null + ? '' + : expandLiteralStrings(typeof json === 'string' ? json : JSON.stringify(json, null, 2)) + ); + + return { + xJson, + setXJson, + convertToJson: collapseLiteralStrings, + }; +}; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 7e5510d7c9c65..4ab791289dd88 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -47,6 +47,10 @@ export { expandLiteralStrings, } from './console_lang'; +import * as Monaco from './monaco'; + +export { Monaco }; + export { AuthorizationContext, AuthorizationProvider, diff --git a/src/plugins/es_ui_shared/public/monaco/index.ts b/src/plugins/es_ui_shared/public/monaco/index.ts new file mode 100644 index 0000000000000..23ba93e913234 --- /dev/null +++ b/src/plugins/es_ui_shared/public/monaco/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useXJsonMode } from '../../__packages_do_not_import__/monaco'; diff --git a/src/plugins/es_ui_shared/static/ace_x_json/hooks/use_x_json.ts b/src/plugins/es_ui_shared/static/ace_x_json/hooks/use_x_json.ts index 2b0bf0c8a3a7c..3a093ac6869d0 100644 --- a/src/plugins/es_ui_shared/static/ace_x_json/hooks/use_x_json.ts +++ b/src/plugins/es_ui_shared/static/ace_x_json/hooks/use_x_json.ts @@ -16,23 +16,18 @@ * specific language governing permissions and limitations * under the License. */ - -import { useState } from 'react'; -import { XJsonMode, collapseLiteralStrings, expandLiteralStrings } from '../../../public'; +import { XJsonMode } from '../../../public'; +import { useXJsonMode as useBaseXJsonMode } from '../../../__packages_do_not_import__/xjson'; const xJsonMode = new XJsonMode(); -export const useXJsonMode = (json: Record | string | null) => { - const [xJson, setXJson] = useState(() => - json === null - ? '' - : expandLiteralStrings(typeof json === 'string' ? json : JSON.stringify(json, null, 2)) - ); +interface ReturnValue extends ReturnType { + xJsonMode: typeof xJsonMode; +} +export const useXJsonMode = (json: Parameters[0]): ReturnValue => { return { - xJson, - setXJson, + ...useBaseXJsonMode(json), xJsonMode, - convertToJson: collapseLiteralStrings, }; }; diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index 0b505f117a580..d5039d0adb318 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -18,7 +18,7 @@ */ import { ExpressionValue, ExpressionValueError } from '../expression_types'; -import { ExpressionFunction } from '../../public'; +import { ExpressionFunction } from '../../common'; export type ExpressionAstNode = | ExpressionAstExpression diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 6a2f4bb269ff3..2e83d16dd778e 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -20,7 +20,7 @@ import { Execution } from './execution'; import { parseExpression, ExpressionAstExpression } from '../ast'; import { createUnitTestExecutor } from '../test_helpers'; -import { ExpressionFunctionDefinition } from '../../public'; +import { ExpressionFunctionDefinition } from '../../common'; import { ExecutionContract } from './execution_contract'; beforeAll(() => { diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index bc27b0eda4959..876e7dfec799c 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ExpressionValueError } from '../../public'; +import { ExpressionValueError } from '../../common'; type ErrorLike = Partial>; diff --git a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap index 924de9bbd0994..2545bbcb5114d 100644 --- a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap @@ -202,17 +202,17 @@ exports[`apmUiEnabled 1`] = ` } textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> @@ -277,7 +277,7 @@ exports[`apmUiEnabled 1`] = ` /> } textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> @@ -543,7 +543,7 @@ exports[`isNewKibanaInstance 1`] = ` />
} textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> @@ -876,7 +876,7 @@ exports[`mlEnabled 1`] = ` /> } textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> @@ -1142,7 +1142,7 @@ exports[`render 1`] = ` /> { const basePath = getServices().getBasePath(); + const renderCards = () => { const apmData = { title: intl.formatMessage({ @@ -79,11 +80,11 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { }; const siemData = { title: intl.formatMessage({ - id: 'home.addData.siem.nameTitle', - defaultMessage: 'SIEM', + id: 'home.addData.securitySolution.nameTitle', + defaultMessage: 'Security', }), description: intl.formatMessage({ - id: 'home.addData.siem.nameDescription', + id: 'home.addData.securitySolution.nameDescription', defaultMessage: 'Centralize security events for interactive investigation in ready-to-go visualizations.', }), @@ -220,11 +221,11 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { footer={ @@ -296,7 +297,7 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { { id="home.dataManagementDisableCollection" defaultMessage=" To stop collection, " /> - + { id="home.dataManagementEnableCollection" defaultMessage=" To start collection, " /> - + { - history.push(`${url}?_a=(tab:${field?.scripted ? TAB_SCRIPTED_FIELDS : TAB_INDEXED_FIELDS})`); + history.push( + `${url}#/?_a=(tab:${field?.scripted ? TAB_SCRIPTED_FIELDS : TAB_INDEXED_FIELDS})` + ); }; if (field) { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx index 11fdae39aee3c..a0d6a43d6f776 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx @@ -20,6 +20,8 @@ import React from 'react'; import { render } from 'enzyme'; import { RouteComponentProps } from 'react-router-dom'; +import { ScopedHistory } from 'kibana/public'; +import { scopedHistoryMock } from '../../../../../../../../core/public/mocks'; import { Header } from './header'; @@ -28,7 +30,7 @@ describe('Header', () => { const component = render( diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.tsx index dc48f61d1aa65..e432b9b466367 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/header.tsx @@ -22,9 +22,13 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ScopedHistory } from 'kibana/public'; + +import { reactRouterNavigate } from '../../../../../../../kibana_react/public'; interface HeaderProps extends RouteComponentProps { indexPatternId: string; + history: ScopedHistory; } export const Header = withRouter(({ indexPatternId, history }: HeaderProps) => ( @@ -52,9 +56,7 @@ export const Header = withRouter(({ indexPatternId, history }: HeaderProps) => ( { - history.push(`${indexPatternId}/create-field/`); - }} + {...reactRouterNavigate(history, `patterns/${indexPatternId}/create-field/`)} > fieldWildcardMatcher(filters, uiSettings.get('metaFields')), + (filters: string[]) => fieldWildcardMatcher(filters, uiSettings.get(UI_SETTINGS.META_FIELDS)), [uiSettings] ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index 3088990aae981..52cd5b0c3f5bd 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts @@ -117,7 +117,7 @@ export function getTabs( } export function getPath(field: IndexPatternField) { - return `${field.indexPattern?.id}/field/${field.name}`; + return `/patterns/${field.indexPattern?.id}/field/${field.name}`; } const allTypesDropDown = i18n.translate( diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx index a1b7289efee21..c97f19f59d340 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx @@ -35,7 +35,12 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { esQuery, IndexPattern, Query } from '../../../../../../../plugins/data/public'; +import { + esQuery, + IndexPattern, + Query, + UI_SETTINGS, +} from '../../../../../../../plugins/data/public'; import { context as contextType } from '../../../../../../kibana_react/public'; import { IndexPatternManagmentContextValue } from '../../../../types'; import { ExecuteScript } from '../../types'; @@ -244,7 +249,7 @@ export class TestScript extends Component { showDatePicker={false} showQueryInput={true} query={{ - language: this.context.services.uiSettings.get('search:queryLanguage'), + language: this.context.services.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '', }} onQuerySubmit={this.previewScript} diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 32ed33550fdcf..947882b8df495 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -27,12 +27,13 @@ import { EuiPanel, EuiSpacer, EuiText, + EuiBadgeGroup, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { CreateButton } from '../create_button'; import { CreateIndexPatternPrompt } from '../create_index_pattern_prompt'; @@ -40,35 +41,6 @@ import { IndexPatternTableItem, IndexPatternCreationOption } from '../types'; import { getIndexPatterns } from '../utils'; import { getListBreadcrumbs } from '../breadcrumbs'; -const columns = [ - { - field: 'title', - name: 'Pattern', - render: ( - name: string, - index: { - id: string; - tags?: Array<{ - key: string; - name: string; - }>; - } - ) => ( - - {name} - {index.tags && - index.tags.map(({ key: tagKey, name: tagName }) => ( - - {tagName} - - ))} - - ), - dataType: 'string' as const, - sortable: ({ sort }: { sort: string }) => sort, - }, -]; - const pagination = { initialPageSize: 10, pageSizeOptions: [5, 10, 25, 50], @@ -140,6 +112,39 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { chrome.docTitle.change(title); + const columns = [ + { + field: 'title', + name: 'Pattern', + render: ( + name: string, + index: { + id: string; + tags?: Array<{ + key: string; + name: string; + }>; + } + ) => ( + <> + + {name} + + + {index.tags && + index.tags.map(({ key: tagKey, name: tagName }) => ( + + {tagName} + + ))} + + + ), + dataType: 'string' as const, + sortable: ({ sort }: { sort: string }) => sort, + }, + ]; + const createButton = canSave ? ( - + @@ -93,7 +93,7 @@ export async function mountManagementSection( - + , params.element diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index ebcd92f25c13b..a98cc05a0a80a 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -71,7 +71,7 @@ export class IndexPatternManagementPlugin throw new Error('`kibana` management section not found.'); } - const newAppPath = `kibana#/management/kibana/${IPM_APP_ID}`; + const newAppPath = `management/kibana/${IPM_APP_ID}`; const legacyPatternsPath = 'management/kibana/index_patterns'; kibanaLegacy.forwardApp('management/kibana/index_pattern', newAppPath, (path) => '/create'); diff --git a/src/plugins/inspector/public/views/data/components/data_table.tsx b/src/plugins/inspector/public/views/data/components/data_table.tsx index 69be069272f79..0fdf3d9b13e33 100644 --- a/src/plugins/inspector/public/views/data/components/data_table.tsx +++ b/src/plugins/inspector/public/views/data/components/data_table.tsx @@ -37,6 +37,7 @@ import { DataDownloadOptions } from './download_options'; import { DataViewRow, DataViewColumn } from '../types'; import { TabularData } from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../../share/public'; interface DataTableFormatState { columns: DataViewColumn[]; @@ -58,8 +59,8 @@ export class DataTableFormat extends Componentstate:storeInSessionStorage, advancedSettingsLink: ( - + + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + +const isLeftClickEvent = (event: any) => event.button === 0; + +export const toLocationObject = (to: string | LocationObject) => + typeof to === 'string' ? { pathname: to } : to; + +export const reactRouterNavigate = ( + history: ScopedHistory | History, + to: string | LocationObject, + onClickCallback?: Function +) => ({ + href: history.createHref(toLocationObject(to)), + onClick: reactRouterOnClickHandler(history, toLocationObject(to), onClickCallback), +}); + +export const reactRouterOnClickHandler = ( + history: ScopedHistory | History, + to: string | LocationObject, + onClickCallback?: Function +) => (event: any) => { + if (onClickCallback) { + onClickCallback(event); + } + + if (event.defaultPrevented) { + return; + } + + if (event.target.getAttribute('target')) { + return; + } + + if (isModifiedEvent(event) || !isLeftClickEvent(event)) { + return; + } + + // prevents page reload + event.preventDefault(); + history.push(toLocationObject(to)); +}; diff --git a/src/plugins/kibana_utils/public/core/create_start_service_getter.ts b/src/plugins/kibana_utils/public/core/create_start_service_getter.ts index 14e2588a0a9cf..5e4a0f2bc7aeb 100644 --- a/src/plugins/kibana_utils/public/core/create_start_service_getter.ts +++ b/src/plugins/kibana_utils/public/core/create_start_service_getter.ts @@ -19,16 +19,17 @@ import { CoreStart, StartServicesAccessor } from '../../../../core/public'; -export interface StartServices { +export interface StartServices { plugins: Plugins; self: OwnContract; - core: CoreStart; + core: Core; } -export type StartServicesGetter = () => StartServices< - Plugins, - OwnContract ->; +export type StartServicesGetter< + Plugins = unknown, + OwnContract = unknown, + Core = CoreStart +> = () => StartServices; /** * Use this utility to create a synchronous *start* service getter in *setup* diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index 8db1d60f09d72..a8c3aab2202d1 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -35,7 +35,7 @@ import { ScopedHistory } from '../../../../../core/public'; describe('kbn_url_storage', () => { describe('getStateFromUrl & setStateToUrl', () => { - const url = 'http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id'; + const url = 'http://localhost:5601/oxf/app/kibana#/yourApp'; const state1 = { testStr: '123', testNumber: 0, @@ -50,14 +50,14 @@ describe('kbn_url_storage', () => { it('should set expanded state to url', () => { let newUrl = setStateToKbnUrl('_s', state1, { useHash: false }, url); expect(newUrl).toMatchInlineSnapshot( - `"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')"` + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')"` ); const retrievedState1 = getStateFromKbnUrl('_s', newUrl); expect(retrievedState1).toEqual(state1); newUrl = setStateToKbnUrl('_s', state2, { useHash: false }, newUrl); expect(newUrl).toMatchInlineSnapshot( - `"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=(test:'123')"` + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(test:'123')"` ); const retrievedState2 = getStateFromKbnUrl('_s', newUrl); expect(retrievedState2).toEqual(state2); @@ -66,14 +66,14 @@ describe('kbn_url_storage', () => { it('should set hashed state to url', () => { let newUrl = setStateToKbnUrl('_s', state1, { useHash: true }, url); expect(newUrl).toMatchInlineSnapshot( - `"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=h@a897fac"` + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@a897fac"` ); const retrievedState1 = getStateFromKbnUrl('_s', newUrl); expect(retrievedState1).toEqual(state1); newUrl = setStateToKbnUrl('_s', state2, { useHash: true }, newUrl); expect(newUrl).toMatchInlineSnapshot( - `"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=h@40f94d5"` + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@40f94d5"` ); const retrievedState2 = getStateFromKbnUrl('_s', newUrl); expect(retrievedState2).toEqual(state2); @@ -244,67 +244,55 @@ describe('kbn_url_storage', () => { it('should extract path relative to browser history without basename', () => { const history = createBrowserHistory(); const url = - "http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + "http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; const relativePath = getRelativeToHistoryPath(url, history); expect(relativePath).toEqual( - "/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + "/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" ); }); it('should extract path relative to browser history with basename', () => { const url = - "http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + "http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; const history1 = createBrowserHistory({ basename: '/oxf/app/' }); const relativePath1 = getRelativeToHistoryPath(url, history1); expect(relativePath1).toEqual( - "/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" + "/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" ); const history2 = createBrowserHistory({ basename: '/oxf/app/kibana/' }); const relativePath2 = getRelativeToHistoryPath(url, history2); - expect(relativePath2).toEqual( - "#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" - ); + expect(relativePath2).toEqual("#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"); }); it('should extract path relative to browser history with basename from relative url', () => { const history = createBrowserHistory({ basename: '/oxf/app/' }); - const url = - "/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const url = "/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; const relativePath = getRelativeToHistoryPath(url, history); - expect(relativePath).toEqual( - "/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" - ); + expect(relativePath).toEqual("/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"); }); it('should extract path relative to hash history without basename', () => { const history = createHashHistory(); const url = - "http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + "http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; const relativePath = getRelativeToHistoryPath(url, history); - expect(relativePath).toEqual( - "/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" - ); + expect(relativePath).toEqual("/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"); }); it('should extract path relative to hash history with basename', () => { const history = createHashHistory({ basename: 'management' }); const url = - "http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + "http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; const relativePath = getRelativeToHistoryPath(url, history); - expect(relativePath).toEqual( - "/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" - ); + expect(relativePath).toEqual("/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"); }); it('should extract path relative to hash history with basename from relative url', () => { const history = createHashHistory({ basename: 'management' }); - const url = - "/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; + const url = "/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"; const relativePath = getRelativeToHistoryPath(url, history); - expect(relativePath).toEqual( - "/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')" - ); + expect(relativePath).toEqual("/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"); }); }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 20816c08c550e..d9149095a2fa2 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -31,7 +31,7 @@ import { url as urlUtils } from '../../../common'; * e.g.: * * given an url: - * http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') + * http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') * will return object: * {_a: {tab: 'indexedFields'}, _b: {f: 'test', i: '', l: ''}}; */ @@ -57,7 +57,7 @@ export function getStatesFromKbnUrl( * e.g.: * * given an url: - * http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') + * http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') * and key '_a' * will return object: * {tab: 'indexedFields'} @@ -74,12 +74,12 @@ export function getStateFromKbnUrl( * Doesn't actually updates history * * e.g.: - * given a url: http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') + * given a url: http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') * key: '_a' * and state: {tab: 'other'} * * will return url: - * http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:other)&_b=(f:test,i:'',l:'') + * http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:other)&_b=(f:test,i:'',l:'') */ export function setStateToKbnUrl( key: string, diff --git a/src/plugins/management/public/__snapshots__/management_app.test.tsx.snap b/src/plugins/management/public/__snapshots__/management_app.test.tsx.snap deleted file mode 100644 index 7f13472ee02ee..0000000000000 --- a/src/plugins/management/public/__snapshots__/management_app.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Management app can mount and unmount 1`] = ` -
-
- Test App - Hello world! -
-
-`; - -exports[`Management app can mount and unmount 2`] = `
`; diff --git a/src/plugins/management/public/application.tsx b/src/plugins/management/public/application.tsx new file mode 100644 index 0000000000000..5d014504b8938 --- /dev/null +++ b/src/plugins/management/public/application.tsx @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { ManagementApp, ManagementAppDependencies } from './components/management_app'; + +export const renderApp = async ( + context: AppMountContext, + { history, appBasePath, element }: AppMountParameters, + dependencies: ManagementAppDependencies +) => { + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/plugins/management/public/components/_index.scss b/src/plugins/management/public/components/_index.scss deleted file mode 100644 index df0ebb48803d9..0000000000000 --- a/src/plugins/management/public/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './management_sidebar_nav/index'; diff --git a/src/plugins/management/public/components/index.ts b/src/plugins/management/public/components/index.ts index 2650d23d3c25c..8979809c5245e 100644 --- a/src/plugins/management/public/components/index.ts +++ b/src/plugins/management/public/components/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { ManagementSidebarNav } from './management_sidebar_nav'; -export { ManagementChrome } from './management_chrome'; +export { ManagementApp } from './management_app'; +export { managementSections } from './management_sections'; diff --git a/src/plugins/management/public/components/landing/index.ts b/src/plugins/management/public/components/landing/index.ts new file mode 100644 index 0000000000000..79a1c2b1145c2 --- /dev/null +++ b/src/plugins/management/public/components/landing/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ManagementLandingPage } from './landing'; diff --git a/src/plugins/management/public/components/landing/landing.tsx b/src/plugins/management/public/components/landing/landing.tsx new file mode 100644 index 0000000000000..f15374173e5f3 --- /dev/null +++ b/src/plugins/management/public/components/landing/landing.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiHorizontalRule, + EuiIcon, + EuiPageContent, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +interface ManagementLandingPageProps { + version: string; + setBreadcrumbs: () => void; +} + +export const ManagementLandingPage = ({ version, setBreadcrumbs }: ManagementLandingPageProps) => { + setBreadcrumbs(); + + return ( + +
+
+ + + +

+ +

+
+ + + +
+ + + + +

+ +

+
+
+
+ ); +}; diff --git a/src/plugins/management/public/components/management_app/index.ts b/src/plugins/management/public/components/management_app/index.ts new file mode 100644 index 0000000000000..83f8ae0159978 --- /dev/null +++ b/src/plugins/management/public/components/management_app/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ManagementApp, ManagementAppDependencies } from './management_app'; diff --git a/src/plugins/management/public/components/management_app/management_app.scss b/src/plugins/management/public/components/management_app/management_app.scss new file mode 100644 index 0000000000000..00b3e51fb53ee --- /dev/null +++ b/src/plugins/management/public/components/management_app/management_app.scss @@ -0,0 +1,6 @@ + +// Hack because the management wrapper is flat HTML and needs a class +.mgtPage__body { + max-width: map-get($euiBreakpoints, 'xl'); + margin: 0 auto; +} diff --git a/src/plugins/management/public/components/management_app/management_app.tsx b/src/plugins/management/public/components/management_app/management_app.tsx new file mode 100644 index 0000000000000..fc5a8924c95d6 --- /dev/null +++ b/src/plugins/management/public/components/management_app/management_app.tsx @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState, useEffect, useCallback } from 'react'; +import { + AppMountContext, + AppMountParameters, + ChromeBreadcrumb, + ScopedHistory, +} from 'kibana/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { EuiPage } from '@elastic/eui'; +import { ManagementStart } from '../../types'; +import { ManagementSection, MANAGEMENT_BREADCRUMB } from '../../utils'; + +import { ManagementRouter } from './management_router'; +import { ManagementSidebarNav } from '../management_sidebar_nav'; +import { reactRouterNavigate } from '../../../../kibana_react/public'; + +import './management_app.scss'; + +interface ManagementAppProps { + appBasePath: string; + context: AppMountContext; + history: AppMountParameters['history']; + dependencies: ManagementAppDependencies; +} + +export interface ManagementAppDependencies { + management: ManagementStart; + kibanaVersion: string; +} + +export const ManagementApp = ({ context, dependencies, history }: ManagementAppProps) => { + const [selectedId, setSelectedId] = useState(''); + const [sections, setSections] = useState(); + + const onAppMounted = useCallback((id: string) => { + setSelectedId(id); + window.scrollTo(0, 0); + }, []); + + const setBreadcrumbs = useCallback( + (crumbs: ChromeBreadcrumb[] = [], appHistory?: ScopedHistory) => { + const wrapBreadcrumb = (item: ChromeBreadcrumb, scopedHistory: ScopedHistory) => ({ + ...item, + ...(item.href ? reactRouterNavigate(scopedHistory, item.href) : {}), + }); + + context.core.chrome.setBreadcrumbs([ + wrapBreadcrumb(MANAGEMENT_BREADCRUMB, history), + ...crumbs.map((item) => wrapBreadcrumb(item, appHistory || history)), + ]); + }, + [context.core.chrome, history] + ); + + useEffect(() => { + setSections(dependencies.management.sections.getSectionsEnabled()); + }, [dependencies.management.sections]); + + if (!sections) { + return null; + } + + return ( + + + + + + + ); +}; diff --git a/src/plugins/management/public/components/management_app/management_router.tsx b/src/plugins/management/public/components/management_app/management_router.tsx new file mode 100644 index 0000000000000..3f934fa68c6ba --- /dev/null +++ b/src/plugins/management/public/components/management_app/management_router.tsx @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { memo } from 'react'; +import { Route, Router, Switch } from 'react-router-dom'; +import { EuiPageBody } from '@elastic/eui'; +import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from 'kibana/public'; +import { ManagementAppWrapper } from '../management_app_wrapper'; +import { ManagementLandingPage } from '../landing'; +import { ManagementAppDependencies } from './management_app'; +import { ManagementSection } from '../../utils'; + +interface ManagementRouterProps { + history: AppMountParameters['history']; + dependencies: ManagementAppDependencies; + setBreadcrumbs: (crumbs?: ChromeBreadcrumb[], appHistory?: ScopedHistory) => void; + onAppMounted: (id: string) => void; + sections: ManagementSection[]; +} + +export const ManagementRouter = memo( + ({ dependencies, history, setBreadcrumbs, onAppMounted, sections }: ManagementRouterProps) => ( + + + + {sections.map((section) => + section + .getAppsEnabled() + .map((app) => ( + ( + + )} + /> + )) + )} + ( + + )} + /> + + + + ) +); diff --git a/src/plugins/management/public/components/management_app_wrapper/index.tsx b/src/plugins/management/public/components/management_app_wrapper/index.tsx new file mode 100644 index 0000000000000..71546e7ca1342 --- /dev/null +++ b/src/plugins/management/public/components/management_app_wrapper/index.tsx @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ManagementAppWrapper } from './management_app_wrapper'; diff --git a/src/plugins/management/public/components/management_app_wrapper/management_app_wrapper.tsx b/src/plugins/management/public/components/management_app_wrapper/management_app_wrapper.tsx new file mode 100644 index 0000000000000..02da2a46540c2 --- /dev/null +++ b/src/plugins/management/public/components/management_app_wrapper/management_app_wrapper.tsx @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { createRef, Component } from 'react'; + +import { ChromeBreadcrumb, AppMountParameters, ScopedHistory } from 'kibana/public'; +import { ManagementApp } from '../../utils'; +import { Unmount } from '../../types'; + +interface ManagementSectionWrapperProps { + app: ManagementApp; + setBreadcrumbs: (crumbs?: ChromeBreadcrumb[], history?: ScopedHistory) => void; + onAppMounted: (id: string) => void; + history: AppMountParameters['history']; +} + +export class ManagementAppWrapper extends Component { + private unmount?: Unmount; + private mountElementRef = createRef(); + + componentDidMount() { + const { setBreadcrumbs, app, onAppMounted, history } = this.props; + const { mount, basePath } = app; + const appHistory = history.createSubHistory(app.basePath); + + const mountResult = mount({ + basePath, + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => setBreadcrumbs(crumbs, appHistory), + element: this.mountElementRef.current!, + history: appHistory, + }); + + onAppMounted(app.id); + + if (mountResult instanceof Promise) { + mountResult.then((um) => { + this.unmount = um; + }); + } else { + this.unmount = mountResult; + } + } + + async componentWillUnmount() { + if (this.unmount) { + await this.unmount(); + } + } + + render() { + return
; + } +} diff --git a/src/plugins/management/public/components/management_chrome/index.ts b/src/plugins/management/public/components/management_chrome/index.ts deleted file mode 100644 index b82c1af871be7..0000000000000 --- a/src/plugins/management/public/components/management_chrome/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { ManagementChrome } from './management_chrome'; diff --git a/src/plugins/management/public/components/management_chrome/management_chrome.tsx b/src/plugins/management/public/components/management_chrome/management_chrome.tsx deleted file mode 100644 index df844e2208936..0000000000000 --- a/src/plugins/management/public/components/management_chrome/management_chrome.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as React from 'react'; -import { EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui'; -import { I18nProvider } from '@kbn/i18n/react'; -import { ManagementSidebarNav } from '../management_sidebar_nav'; -import { LegacySection } from '../../types'; -import { ManagementSection } from '../../management_section'; - -interface Props { - getSections: () => ManagementSection[]; - legacySections: LegacySection[]; - selectedId: string; - onMounted: (element: HTMLDivElement) => void; -} - -export class ManagementChrome extends React.Component { - private container = React.createRef(); - componentDidMount() { - if (this.container.current) { - this.props.onMounted(this.container.current); - } - } - render() { - return ( - - - - - - -
- - - - ); - } -} diff --git a/src/plugins/management/public/management_sections.tsx b/src/plugins/management/public/components/management_sections.tsx similarity index 93% rename from src/plugins/management/public/management_sections.tsx rename to src/plugins/management/public/components/management_sections.tsx index 77e494626a00e..33c3526c4d23b 100644 --- a/src/plugins/management/public/management_sections.tsx +++ b/src/plugins/management/public/components/management_sections.tsx @@ -20,14 +20,14 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiIcon } from '@elastic/eui'; -import { ManagementSectionId } from './types'; +import { ManagementSectionId } from '../types'; -interface Props { +interface ManagementSectionTitleProps { text: string; tip: string; } -const ManagementSectionTitle = ({ text, tip }: Props) => ( +const ManagementSectionTitle = ({ text, tip }: ManagementSectionTitleProps) => ( {text} diff --git a/src/plugins/management/public/components/management_sidebar_nav/__snapshots__/management_sidebar_nav.test.ts.snap b/src/plugins/management/public/components/management_sidebar_nav/__snapshots__/management_sidebar_nav.test.ts.snap deleted file mode 100644 index e7225b356ed68..0000000000000 --- a/src/plugins/management/public/components/management_sidebar_nav/__snapshots__/management_sidebar_nav.test.ts.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Management adds legacy apps to existing SidebarNav sections 1`] = ` -Array [ - Object { - "data-test-subj": "activeSection", - "icon": null, - "id": "activeSection", - "items": Array [ - Object { - "data-test-subj": "item", - "href": undefined, - "id": "item", - "isSelected": false, - "name": "item", - "order": undefined, - }, - ], - "name": "activeSection", - "order": 10, - }, - Object { - "data-test-subj": "no-active-items", - "icon": null, - "id": "no-active-items", - "items": Array [ - Object { - "data-test-subj": "disabled", - "href": undefined, - "id": "disabled", - "isSelected": false, - "name": "disabled", - "order": undefined, - }, - Object { - "data-test-subj": "notVisible", - "href": undefined, - "id": "notVisible", - "isSelected": false, - "name": "notVisible", - "order": undefined, - }, - ], - "name": "No active items", - "order": 10, - }, -] -`; - -exports[`Management maps legacy sections and apps into SidebarNav items 1`] = ` -Array [ - Object { - "data-test-subj": "no-active-items", - "icon": null, - "id": "no-active-items", - "items": Array [ - Object { - "data-test-subj": "disabled", - "href": undefined, - "id": "disabled", - "isSelected": false, - "name": "disabled", - "order": undefined, - }, - Object { - "data-test-subj": "notVisible", - "href": undefined, - "id": "notVisible", - "isSelected": false, - "name": "notVisible", - "order": undefined, - }, - ], - "name": "No active items", - "order": 10, - }, - Object { - "data-test-subj": "activeSection", - "icon": null, - "id": "activeSection", - "items": Array [ - Object { - "data-test-subj": "item", - "href": undefined, - "id": "item", - "isSelected": false, - "name": "item", - "order": undefined, - }, - ], - "name": "activeSection", - "order": 10, - }, -] -`; diff --git a/src/plugins/management/public/components/management_sidebar_nav/_index.scss b/src/plugins/management/public/components/management_sidebar_nav/_index.scss deleted file mode 100644 index 0a48807344abd..0000000000000 --- a/src/plugins/management/public/components/management_sidebar_nav/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './sidebar_nav'; \ No newline at end of file diff --git a/src/plugins/management/public/components/management_sidebar_nav/_sidebar_nav.scss b/src/plugins/management/public/components/management_sidebar_nav/_sidebar_nav.scss deleted file mode 100644 index 5302bd200de3f..0000000000000 --- a/src/plugins/management/public/components/management_sidebar_nav/_sidebar_nav.scss +++ /dev/null @@ -1,10 +0,0 @@ -.mgtSideBarNav { - width: 210px; -} - -@include euiBreakpoint('xs','s') { - .mgtSideBarNav { - width: auto; - margin-bottom: $euiSize; - } -} diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.scss b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.scss new file mode 100644 index 0000000000000..a148c1e141e8d --- /dev/null +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.scss @@ -0,0 +1,11 @@ +.mgtSideBarNav { + width: 210px; + margin-right: $euiSize; +} + +@include euiBreakpoint('xs','s') { + .mgtSideBarNav { + width: auto; + margin-bottom: $euiSize; + } +} diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.test.ts b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.test.ts deleted file mode 100644 index e04e0a7572612..0000000000000 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IndexedArray } from '../../../../../legacy/ui/public/indexed_array'; -import { mergeLegacyItems } from './management_sidebar_nav'; - -const toIndexedArray = (initialSet: any[]) => - new IndexedArray({ - index: ['id'], - order: ['order'], - initialSet, - }); - -const activeProps = { visible: true, disabled: false }; -const disabledProps = { visible: true, disabled: true }; -const notVisibleProps = { visible: false, disabled: false }; -const visibleItem = { display: 'item', id: 'item', ...activeProps }; - -const notVisibleSection = { - display: 'Not visible', - id: 'not-visible', - order: 10, - visibleItems: toIndexedArray([visibleItem]), - ...notVisibleProps, -}; -const disabledSection = { - display: 'Disabled', - id: 'disabled', - order: 10, - visibleItems: toIndexedArray([visibleItem]), - ...disabledProps, -}; -const noItemsSection = { - display: 'No items', - id: 'no-items', - order: 10, - visibleItems: toIndexedArray([]), - ...activeProps, -}; -const noActiveItemsSection = { - display: 'No active items', - id: 'no-active-items', - order: 10, - visibleItems: toIndexedArray([ - { display: 'disabled', id: 'disabled', ...disabledProps }, - { display: 'notVisible', id: 'notVisible', ...notVisibleProps }, - ]), - ...activeProps, -}; -const activeSection = { - display: 'activeSection', - id: 'activeSection', - order: 10, - visibleItems: toIndexedArray([visibleItem]), - ...activeProps, -}; - -const managementSections = [ - notVisibleSection, - disabledSection, - noItemsSection, - noActiveItemsSection, - activeSection, -]; - -describe('Management', () => { - it('maps legacy sections and apps into SidebarNav items', () => { - expect(mergeLegacyItems([], managementSections, 'active-item-id')).toMatchSnapshot(); - }); - - it('adds legacy apps to existing SidebarNav sections', () => { - const navSection = { - 'data-test-subj': 'activeSection', - icon: null, - id: 'activeSection', - items: [], - name: 'activeSection', - order: 10, - }; - expect(mergeLegacyItems([navSection], managementSections, 'active-item-id')).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index 208f577b76996..055dda5ed84a1 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -17,184 +17,97 @@ * under the License. */ -import { - EuiIcon, - // @ts-ignore - EuiSideNav, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import React, { ReactElement } from 'react'; -import { LegacySection, LegacyApp } from '../../types'; -import { ManagementApp } from '../../management_app'; -import { ManagementSection } from '../../management_section'; - -interface NavApp { - id: string; - name: ReactElement | string; - [key: string]: unknown; - order: number; // only needed while merging platform and legacy -} +import { sortBy } from 'lodash'; -interface NavSection extends NavApp { - items: NavApp[]; -} +import { EuiIcon, EuiSideNav, EuiScreenReaderOnly, EuiSideNavItemType } from '@elastic/eui'; +import { AppMountParameters } from 'kibana/public'; +import { ManagementApp, ManagementSection } from '../../utils'; + +import './management_sidebar_nav.scss'; + +import { ManagementItem } from '../../utils/management_item'; +import { reactRouterNavigate } from '../../../../kibana_react/public'; interface ManagementSidebarNavProps { - getSections: () => ManagementSection[]; - legacySections: LegacySection[]; + sections: ManagementSection[]; + history: AppMountParameters['history']; selectedId: string; } -interface ManagementSidebarNavState { - isSideNavOpenOnMobile: boolean; -} - -const managementSectionOrAppToNav = (appOrSection: ManagementApp | ManagementSection) => ({ - id: appOrSection.id, - name: appOrSection.title, - 'data-test-subj': appOrSection.id, - order: appOrSection.order, -}); - -const managementSectionToNavSection = (section: ManagementSection) => { - const iconType = section.euiIconType - ? section.euiIconType - : section.icon - ? section.icon - : 'empty'; - - return { - icon: , - ...managementSectionOrAppToNav(section), - }; -}; - -const managementAppToNavItem = (selectedId?: string, parentId?: string) => ( - app: ManagementApp -) => ({ - isSelected: selectedId === app.id, - href: `#/management/${parentId}/${app.id}`, - ...managementSectionOrAppToNav(app), +const headerLabel = i18n.translate('management.nav.label', { + defaultMessage: 'Management', }); -const legacySectionToNavSection = (section: LegacySection) => ({ - name: section.display, - id: section.id, - icon: section.icon ? : null, - items: [], - 'data-test-subj': section.id, - // @ts-ignore - order: section.order, +const navMenuLabel = i18n.translate('management.nav.menu', { + defaultMessage: 'Management menu', }); -const legacyAppToNavItem = (app: LegacyApp, selectedId: string) => ({ - isSelected: selectedId === app.id, - name: app.display, - id: app.id, - href: app.url, - 'data-test-subj': app.id, - // @ts-ignore - order: app.order, -}); - -const sectionVisible = (section: LegacySection | LegacyApp) => !section.disabled && section.visible; - -const sideNavItems = (sections: ManagementSection[], selectedId: string) => - sections.map((section) => ({ - items: section.getAppsEnabled().map(managementAppToNavItem(selectedId, section.id)), - ...managementSectionToNavSection(section), - })); - -const findOrAddSection = (navItems: NavSection[], legacySection: LegacySection): NavSection => { - const foundSection = navItems.find((sec) => sec.id === legacySection.id); - - if (foundSection) { - return foundSection; - } else { - const newSection = legacySectionToNavSection(legacySection); - navItems.push(newSection); - navItems.sort((a: NavSection, b: NavSection) => a.order - b.order); // only needed while merging platform and legacy - return newSection; - } -}; - -export const mergeLegacyItems = ( - navItems: NavSection[], - legacySections: LegacySection[], - selectedId: string -) => { - const filteredLegacySections = legacySections - .filter(sectionVisible) - .filter((section) => section.visibleItems.length); - - filteredLegacySections.forEach((legacySection) => { - const section = findOrAddSection(navItems, legacySection); - legacySection.visibleItems.forEach((app) => { - section.items.push(legacyAppToNavItem(app, selectedId)); - return section.items.sort((a, b) => a.order - b.order); - }); - }); - - return navItems; -}; - -const sectionsToItems = ( - sections: ManagementSection[], - legacySections: LegacySection[], - selectedId: string -) => { - const navItems = sideNavItems(sections, selectedId); - return mergeLegacyItems(navItems, legacySections, selectedId); -}; +/** @internal **/ +export const ManagementSidebarNav = ({ + selectedId, + sections, + history, +}: ManagementSidebarNavProps) => { + const HEADER_ID = 'stack-management-nav-header'; + const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false); + const toggleOpenOnMobile = () => setIsSideNavOpenOnMobile(!isSideNavOpenOnMobile); + + const sectionsToNavItems = (managementSections: ManagementSection[]) => { + const sortedManagementSections = sortBy(managementSections, 'order'); + + return sortedManagementSections.reduce>>((acc, section) => { + const apps = sortBy(section.getAppsEnabled(), 'order'); + + if (apps.length) { + acc.push({ + ...createNavItem(section, { + items: appsToNavItems(apps), + }), + }); + } + + return acc; + }, []); + }; -export class ManagementSidebarNav extends React.Component< - ManagementSidebarNavProps, - ManagementSidebarNavState -> { - constructor(props: ManagementSidebarNavProps) { - super(props); - this.state = { - isSideNavOpenOnMobile: false, + const appsToNavItems = (managementApps: ManagementApp[]) => + managementApps.map((app) => ({ + ...createNavItem(app, { + ...reactRouterNavigate(history, app.basePath), + }), + })); + + const createNavItem = ( + item: T, + customParams: Partial> = {} + ) => { + const iconType = item.euiIconType || item.icon; + + return { + id: item.id, + name: item.title, + isSelected: item.id === selectedId, + icon: iconType ? : undefined, + 'data-test-subj': item.id, + ...customParams, }; - } - - public render() { - const HEADER_ID = 'stack-management-nav-header'; - - return ( - <> - -

- {i18n.translate('management.nav.label', { - defaultMessage: 'Management', - })} -

-
- - - ); - } - - private renderMobileTitle() { - return ; - } - - private toggleOpenOnMobile = () => { - this.setState({ - isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile, - }); }; -} + + return ( + <> + +

{headerLabel}

+
+ + + ); +}; diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index f2cc6a00b93d8..3ba469c7831f6 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -21,17 +21,14 @@ import { PluginInitializerContext } from 'kibana/public'; import { ManagementPlugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { - return new ManagementPlugin(); + return new ManagementPlugin(initializerContext); } +export { RegisterManagementAppArgs, ManagementSection, ManagementApp } from './utils'; + export { - ManagementSetup, - ManagementStart, - RegisterManagementApp, ManagementSectionId, - RegisterManagementAppArgs, ManagementAppMountParams, + ManagementSetup, + ManagementStart, } from './types'; -export { ManagementApp } from './management_app'; -export { ManagementSection } from './management_section'; -export { ManagementSidebarNav } from './components'; // for use in legacy management apps diff --git a/src/plugins/management/public/legacy/index.js b/src/plugins/management/public/legacy/index.js deleted file mode 100644 index f2e0ba89b7b59..0000000000000 --- a/src/plugins/management/public/legacy/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { LegacyManagementAdapter } from './sections_register'; -export { LegacyManagementSection } from './section'; diff --git a/src/plugins/management/public/legacy/redirect_messages.tsx b/src/plugins/management/public/legacy/redirect_messages.tsx deleted file mode 100644 index f8cb975e6fae5..0000000000000 --- a/src/plugins/management/public/legacy/redirect_messages.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { EuiCallOut } from '@elastic/eui'; -import { NotificationsStart, OverlayStart } from 'kibana/public'; -import { parse } from 'query-string'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../kibana_react/public'; -import { MarkdownSimple } from '../../../kibana_react/public'; - -/** - * Show banners and toasts carried over from other applications. This is only necessary as long as - * management is rendered in the legacy platform (which requires a full page reload to switch to). - * - * Once management is rendered using the core application service, this file and the places setting - * bannerMessage and notFoundMessage URL params can be removed. - * @param notifications Core notifications service - * @param overlays Core overlays service - */ -export function showLegacyRedirectMessages( - notifications: NotificationsStart, - overlays: OverlayStart -) { - const queryPosition = window.location.hash.indexOf('?'); - if (queryPosition === -1) { - return; - } - - const urlParams = parse(window.location.hash.substr(queryPosition)) as Record; - - if (urlParams.bannerMessage) { - const bannerId = overlays.banners.add( - toMountPoint( - - ) - ); - setTimeout(() => { - overlays.banners.remove(bannerId); - }, 15000); - } - - if (urlParams.notFoundMessage) { - notifications.toasts.addWarning({ - title: i18n.translate('management.history.savedObjectIsMissingNotificationMessage', { - defaultMessage: 'Saved object is missing', - }), - text: toMountPoint({urlParams.notFoundMessage}), - }); - } -} diff --git a/src/plugins/management/public/legacy/section.js b/src/plugins/management/public/legacy/section.js deleted file mode 100644 index 5b39f350bf444..0000000000000 --- a/src/plugins/management/public/legacy/section.js +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { assign } from 'lodash'; -import { IndexedArray } from '../../../../legacy/ui/public/indexed_array'; - -const listeners = []; - -export class LegacyManagementSection { - /** - * @param {string} id - * @param {object} options - * @param {number|null} options.order - * @param {string|null} options.display - defaults to id - * @param {string|null} options.url - defaults to '' - * @param {boolean|null} options.visible - defaults to true - * @param {boolean|null} options.disabled - defaults to false - * @param {string|null} options.tooltip - defaults to '' - * @param {string|null} options.icon - defaults to '' - * @returns {ManagementSection} - */ - - constructor(id, options = {}, capabilities) { - this.display = id; - this.id = id; - this.items = new IndexedArray({ - index: ['id'], - order: ['order'], - }); - this.visible = true; - this.disabled = false; - this.tooltip = ''; - this.icon = ''; - this.url = ''; - this.capabilities = capabilities; - - assign(this, options); - } - - get visibleItems() { - return this.items.inOrder.filter((item) => { - const capabilityManagementSection = this.capabilities.management[this.id]; - const itemCapability = capabilityManagementSection - ? capabilityManagementSection[item.id] - : null; - - return item.visible && itemCapability !== false; - }); - } - - /** - * Registers a callback that will be executed when management sections are updated - * Globally bound to solve for sidebar nav needs - * - * @param {function} fn - */ - addListener(fn) { - listeners.push(fn); - } - - /** - * Registers a sub-section - * - * @param {string} id - * @param {object} options - * @returns {ManagementSection} - */ - - register(id, options = {}) { - const item = new LegacyManagementSection( - id, - assign(options, { parent: this }), - this.capabilities - ); - - if (this.hasItem(id)) { - throw new Error(`'${id}' is already registered`); - } - - this.items.push(item); - listeners.forEach((fn) => fn()); - - return item; - } - - /** - * Deregisters a section - * - * @param {string} id - */ - deregister(id) { - this.items.remove((item) => item.id === id); - listeners.forEach((fn) => fn(this.items)); - } - - /** - * Determine if an id is already registered - * - * @param {string} id - * @returns {boolean} - */ - - hasItem(id) { - return this.items.byId.hasOwnProperty(id); - } - - /** - * Fetches a section by id - * - * @param {string} id - * @returns {ManagementSection} - */ - - getSection(id) { - if (!id) { - return; - } - - const sectionPath = id.split('/'); - return sectionPath.reduce((currentSection, nextSection) => { - if (!currentSection) { - return; - } - - return currentSection.items.byId[nextSection]; - }, this); - } - - hide() { - this.visible = false; - } - - show() { - this.visible = true; - } - - disable() { - this.disabled = true; - } - - enable() { - this.disabled = false; - } -} diff --git a/src/plugins/management/public/legacy/section.test.js b/src/plugins/management/public/legacy/section.test.js deleted file mode 100644 index bf75506a218d5..0000000000000 --- a/src/plugins/management/public/legacy/section.test.js +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyManagementSection } from './section'; -import { IndexedArray } from '../../../../legacy/ui/public/indexed_array'; - -const capabilitiesMock = { - management: { - kibana: { sampleFeature2: false }, - }, -}; - -describe('ManagementSection', () => { - describe('constructor', () => { - it('defaults display to id', () => { - const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - expect(section.display).toBe('kibana'); - }); - - it('defaults visible to true', () => { - const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - expect(section.visible).toBe(true); - }); - - it('defaults disabled to false', () => { - const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - expect(section.disabled).toBe(false); - }); - - it('defaults tooltip to empty string', () => { - const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - expect(section.tooltip).toBe(''); - }); - - it('defaults url to empty string', () => { - const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - expect(section.url).toBe(''); - }); - - it('exposes items', () => { - const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - expect(section.items).toHaveLength(0); - }); - - it('exposes visibleItems', () => { - const section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - expect(section.visibleItems).toHaveLength(0); - }); - - it('assigns all options', () => { - const section = new LegacyManagementSection( - 'kibana', - { description: 'test', url: 'foobar' }, - capabilitiesMock - ); - expect(section.description).toBe('test'); - expect(section.url).toBe('foobar'); - }); - }); - - describe('register', () => { - let section; - - beforeEach(() => { - section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - }); - - it('returns a ManagementSection', () => { - expect(section.register('about')).toBeInstanceOf(LegacyManagementSection); - }); - - it('provides a reference to the parent', () => { - expect(section.register('about').parent).toBe(section); - }); - - it('adds item', function () { - section.register('about', { description: 'test' }); - - expect(section.items).toHaveLength(1); - expect(section.items[0]).toBeInstanceOf(LegacyManagementSection); - expect(section.items[0].id).toBe('about'); - }); - - it('can only register a section once', () => { - let threwException = false; - section.register('about'); - - try { - section.register('about'); - } catch (e) { - threwException = e.message.indexOf('is already registered') > -1; - } - - expect(threwException).toBe(true); - }); - - it('calls listener when item added', () => { - let listerCalled = false; - const listenerFn = () => { - listerCalled = true; - }; - - section.addListener(listenerFn); - section.register('about'); - expect(listerCalled).toBe(true); - }); - }); - - describe('deregister', () => { - let section; - - beforeEach(() => { - section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - section.register('about'); - }); - - it('deregisters an existing section', () => { - section.deregister('about'); - expect(section.items).toHaveLength(0); - }); - - it('allows deregistering a section more than once', () => { - section.deregister('about'); - section.deregister('about'); - expect(section.items).toHaveLength(0); - }); - - it('calls listener when item added', () => { - let listerCalled = false; - const listenerFn = () => { - listerCalled = true; - }; - - section.addListener(listenerFn); - section.deregister('about'); - expect(listerCalled).toBe(true); - }); - }); - - describe('getSection', () => { - let section; - - beforeEach(() => { - section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - section.register('about'); - }); - - it('returns registered section', () => { - expect(section.getSection('about')).toBeInstanceOf(LegacyManagementSection); - }); - - it('returns undefined if un-registered', () => { - expect(section.getSection('unknown')).not.toBeDefined(); - }); - - it('returns sub-sections specified via a /-separated path', () => { - section.getSection('about').register('time'); - expect(section.getSection('about/time')).toBeInstanceOf(LegacyManagementSection); - expect(section.getSection('about/time')).toBe(section.getSection('about').getSection('time')); - }); - - it('returns undefined if a sub-section along a /-separated path does not exist', () => { - expect(section.getSection('about/damn/time')).toBe(undefined); - }); - }); - - describe('items', () => { - let section; - - beforeEach(() => { - section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - - section.register('three', { order: 3 }); - section.register('one', { order: 1 }); - section.register('two', { order: 2 }); - }); - - it('is an indexed array', () => { - expect(section.items).toBeInstanceOf(IndexedArray); - }); - - it('is indexed on id', () => { - const keys = Object.keys(section.items.byId).sort(); - expect(section.items.byId).toBeInstanceOf(Object); - - expect(keys).toEqual(['one', 'three', 'two']); - }); - - it('can be ordered', () => { - const ids = section.items.inOrder.map((i) => { - return i.id; - }); - expect(ids).toEqual(['one', 'two', 'three']); - }); - }); - - describe('visible', () => { - let section; - - beforeEach(() => { - section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - }); - - it('hide sets visible to false', () => { - section.hide(); - expect(section.visible).toBe(false); - }); - - it('show sets visible to true', () => { - section.hide(); - section.show(); - expect(section.visible).toBe(true); - }); - }); - - describe('disabled', () => { - let section; - - beforeEach(() => { - section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - }); - - it('disable sets disabled to true', () => { - section.disable(); - expect(section.disabled).toBe(true); - }); - - it('enable sets disabled to false', () => { - section.enable(); - expect(section.disabled).toBe(false); - }); - }); - - describe('visibleItems', () => { - let section; - - beforeEach(() => { - section = new LegacyManagementSection('kibana', {}, capabilitiesMock); - - section.register('three', { order: 3 }); - section.register('one', { order: 1 }); - section.register('two', { order: 2 }); - }); - - it('maintains the order', () => { - const ids = section.visibleItems.map((i) => { - return i.id; - }); - expect(ids).toEqual(['one', 'two', 'three']); - }); - - it('does not include hidden items', () => { - section.getSection('two').hide(); - - const ids = section.visibleItems.map((i) => { - return i.id; - }); - expect(ids).toEqual(['one', 'three']); - }); - - it('does not include visible items hidden via uiCapabilities', () => { - section.register('sampleFeature2', { order: 4, visible: true }); - const ids = section.visibleItems.map((i) => { - return i.id; - }); - expect(ids).toEqual(['one', 'two', 'three']); - }); - }); -}); diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js deleted file mode 100644 index d77f87e80ea18..0000000000000 --- a/src/plugins/management/public/legacy/sections_register.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; -import { LegacyManagementSection } from './section'; -import { managementSections } from '../management_sections'; - -export class LegacyManagementAdapter { - main = undefined; - init = (capabilities) => { - this.main = new LegacyManagementSection( - 'management', - { - display: i18n.translate('management.displayName', { - defaultMessage: 'Stack Management', - }), - }, - capabilities - ); - - managementSections.forEach(({ id, title }, idx) => { - this.main.register(id, { - display: title, - order: idx, - }); - }); - - return this.main; - }; - getManagement = () => this.main; -} diff --git a/src/plugins/management/public/management_app.test.tsx b/src/plugins/management/public/management_app.test.tsx deleted file mode 100644 index a76b234d95ef5..0000000000000 --- a/src/plugins/management/public/management_app.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import { coreMock } from '../../../core/public/mocks'; - -import { ManagementApp } from './management_app'; -// @ts-ignore -import { LegacyManagementSection } from './legacy'; - -function createTestApp() { - const legacySection = new LegacyManagementSection('legacy'); - return new ManagementApp( - { - id: 'test-app', - title: 'Test App', - basePath: '', - mount(params) { - params.setBreadcrumbs([{ text: 'Test App' }]); - ReactDOM.render(
Test App - Hello world!
, params.element); - - return () => { - ReactDOM.unmountComponentAtNode(params.element); - }; - }, - }, - () => [], - jest.fn(), - () => legacySection, - coreMock.createSetup().getStartServices - ); -} - -test('Management app can mount and unmount', async () => { - const testApp = createTestApp(); - const container = document.createElement('div'); - document.body.appendChild(container); - const unmount = testApp.mount({ element: container, basePath: '', setBreadcrumbs: jest.fn() }); - expect(container).toMatchSnapshot(); - (await unmount)(); - expect(container).toMatchSnapshot(); -}); - -test('Enabled by default, can disable', () => { - const testApp = createTestApp(); - expect(testApp.enabled).toBe(true); - testApp.disable(); - expect(testApp.enabled).toBe(false); -}); diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx deleted file mode 100644 index 2954cefa86d5c..0000000000000 --- a/src/plugins/management/public/management_app.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import { i18n } from '@kbn/i18n'; -import { CreateManagementApp, ManagementSectionMount, Unmount } from './types'; -import { KibanaLegacySetup } from '../../kibana_legacy/public'; -// @ts-ignore -import { LegacyManagementSection } from './legacy'; -import { ManagementChrome } from './components'; -import { ManagementSection } from './management_section'; -import { ChromeBreadcrumb, StartServicesAccessor } from '../../../core/public/'; - -export class ManagementApp { - readonly id: string; - readonly title: string; - readonly basePath: string; - readonly order: number; - readonly mount: ManagementSectionMount; - private enabledStatus = true; - - constructor( - { id, title, basePath, order = 100, mount }: CreateManagementApp, - getSections: () => ManagementSection[], - registerLegacyApp: KibanaLegacySetup['registerLegacyApp'], - getLegacyManagementSections: () => LegacyManagementSection, - getStartServices: StartServicesAccessor - ) { - this.id = id; - this.title = title; - this.basePath = basePath; - this.order = order; - this.mount = mount; - - registerLegacyApp({ - id: basePath.substr(1), // get rid of initial slash - title, - mount: async ({}, params) => { - let appUnmount: Unmount; - if (!this.enabledStatus) { - const [coreStart] = await getStartServices(); - coreStart.application.navigateToApp('kibana#/management'); - return () => {}; - } - async function setBreadcrumbs(crumbs: ChromeBreadcrumb[]) { - const [coreStart] = await getStartServices(); - coreStart.chrome.setBreadcrumbs([ - { - text: i18n.translate('management.breadcrumb', { - defaultMessage: 'Stack Management', - }), - href: '#/management', - }, - ...crumbs, - ]); - } - - ReactDOM.render( - { - appUnmount = await mount({ - basePath, - element, - setBreadcrumbs, - }); - }} - />, - params.element - ); - - return async () => { - appUnmount(); - ReactDOM.unmountComponentAtNode(params.element); - }; - }, - }); - } - public enable() { - this.enabledStatus = true; - } - public disable() { - this.enabledStatus = false; - } - public get enabled() { - return this.enabledStatus; - } -} diff --git a/src/plugins/management/public/management_section.test.ts b/src/plugins/management/public/management_section.test.ts deleted file mode 100644 index e1d047425ac18..0000000000000 --- a/src/plugins/management/public/management_section.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ManagementSection } from './management_section'; -import { ManagementSectionId } from './types'; -// @ts-ignore -import { LegacyManagementSection } from './legacy'; -import { coreMock } from '../../../core/public/mocks'; - -function createSection(registerLegacyApp: () => void) { - const legacySection = new LegacyManagementSection('legacy'); - const getLegacySection = () => legacySection; - const getManagementSections: () => ManagementSection[] = () => []; - - const testSectionConfig = { id: ManagementSectionId.Data, title: 'Test Section' }; - return new ManagementSection( - testSectionConfig, - getManagementSections, - registerLegacyApp, - getLegacySection, - coreMock.createSetup().getStartServices - ); -} - -test('cannot register two apps with the same id', () => { - const registerLegacyApp = jest.fn(); - const section = createSection(registerLegacyApp); - - const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} }; - - section.registerApp(testAppConfig); - expect(registerLegacyApp).toHaveBeenCalled(); - expect(section.apps.length).toEqual(1); - - expect(() => { - section.registerApp(testAppConfig); - }).toThrow(); -}); - -test('can enable and disable apps', () => { - const registerLegacyApp = jest.fn(); - const section = createSection(registerLegacyApp); - - const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} }; - - const app = section.registerApp(testAppConfig); - expect(section.getAppsEnabled().length).toEqual(1); - app.disable(); - expect(section.getAppsEnabled().length).toEqual(0); -}); diff --git a/src/plugins/management/public/management_section.ts b/src/plugins/management/public/management_section.ts deleted file mode 100644 index 80ef1a108ecd8..0000000000000 --- a/src/plugins/management/public/management_section.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ReactElement } from 'react'; - -import { CreateSection, RegisterManagementAppArgs, ManagementSectionId } from './types'; -import { KibanaLegacySetup } from '../../kibana_legacy/public'; -import { StartServicesAccessor } from '../../../core/public'; -// @ts-ignore -import { LegacyManagementSection } from './legacy'; -import { ManagementApp } from './management_app'; - -export class ManagementSection { - public readonly id: ManagementSectionId; - public readonly title: string | ReactElement = ''; - public readonly apps: ManagementApp[] = []; - public readonly order: number; - public readonly euiIconType?: string; - public readonly icon?: string; - private readonly getSections: () => ManagementSection[]; - private readonly registerLegacyApp: KibanaLegacySetup['registerLegacyApp']; - private readonly getLegacyManagementSection: () => LegacyManagementSection; - private readonly getStartServices: StartServicesAccessor; - - constructor( - { id, title, order = 100, euiIconType, icon }: CreateSection, - getSections: () => ManagementSection[], - registerLegacyApp: KibanaLegacySetup['registerLegacyApp'], - getLegacyManagementSection: () => ManagementSection, - getStartServices: StartServicesAccessor - ) { - this.id = id; - this.title = title; - this.order = order; - this.euiIconType = euiIconType; - this.icon = icon; - this.getSections = getSections; - this.registerLegacyApp = registerLegacyApp; - this.getLegacyManagementSection = getLegacyManagementSection; - this.getStartServices = getStartServices; - } - - registerApp({ id, title, order, mount }: RegisterManagementAppArgs) { - if (this.getApp(id)) { - throw new Error(`Management app already registered - id: ${id}, title: ${title}`); - } - - const app = new ManagementApp( - { id, title, order, mount, basePath: `/management/${this.id}/${id}` }, - this.getSections, - this.registerLegacyApp, - this.getLegacyManagementSection, - this.getStartServices - ); - this.apps.push(app); - return app; - } - getApp(id: ManagementApp['id']) { - return this.apps.find((app) => app.id === id); - } - getAppsEnabled() { - return this.apps.filter((app) => app.enabled).sort((a, b) => a.order - b.order); - } -} diff --git a/src/plugins/management/public/management_sections_service.test.ts b/src/plugins/management/public/management_sections_service.test.ts new file mode 100644 index 0000000000000..2c5d04883235c --- /dev/null +++ b/src/plugins/management/public/management_sections_service.test.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ManagementSectionId } from './index'; +import { ManagementSectionsService } from './management_sections_service'; + +describe('ManagementService', () => { + let managementService: ManagementSectionsService; + + beforeEach(() => { + managementService = new ManagementSectionsService(); + }); + + test('Provides default sections', () => { + managementService.setup(); + const start = managementService.start(); + + expect(start.getAllSections().length).toEqual(6); + expect(start.getSection(ManagementSectionId.Ingest)).toBeDefined(); + expect(start.getSection(ManagementSectionId.Data)).toBeDefined(); + expect(start.getSection(ManagementSectionId.InsightsAndAlerting)).toBeDefined(); + expect(start.getSection(ManagementSectionId.Security)).toBeDefined(); + expect(start.getSection(ManagementSectionId.Kibana)).toBeDefined(); + expect(start.getSection(ManagementSectionId.Stack)).toBeDefined(); + }); + + test('Register section, enable and disable', () => { + // Setup phase: + const setup = managementService.setup(); + const testSection = setup.register({ id: 'test-section', title: 'Test Section' }); + + expect(setup.getSection('test-section')).not.toBeUndefined(); + + // Start phase: + const start = managementService.start(); + + expect(start.getSectionsEnabled().length).toEqual(7); + + testSection.disable(); + + expect(start.getSectionsEnabled().length).toEqual(6); + }); +}); diff --git a/src/plugins/management/public/management_sections_service.ts b/src/plugins/management/public/management_sections_service.ts new file mode 100644 index 0000000000000..08a87b3e89f2b --- /dev/null +++ b/src/plugins/management/public/management_sections_service.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ReactElement } from 'react'; +import { ManagementSection, RegisterManagementSectionArgs } from './utils'; +import { managementSections } from './components/management_sections'; + +import { ManagementSectionId, SectionsServiceSetup, SectionsServiceStart } from './types'; + +export class ManagementSectionsService { + private sections: Map = new Map(); + + private getSection = (sectionId: ManagementSectionId | string) => + this.sections.get(sectionId) as ManagementSection; + + private getAllSections = () => [...this.sections.values()]; + + private registerSection = (section: RegisterManagementSectionArgs) => { + if (this.sections.has(section.id)) { + throw Error(`ManagementSection '${section.id}' already registered`); + } + + const newSection = new ManagementSection(section); + + this.sections.set(section.id, newSection); + return newSection; + }; + + setup(): SectionsServiceSetup { + managementSections.forEach( + ({ id, title }: { id: ManagementSectionId; title: ReactElement }, idx: number) => { + this.registerSection({ id, title, order: idx }); + } + ); + + return { + register: this.registerSection, + getSection: this.getSection, + }; + } + + start(): SectionsServiceStart { + return { + getSection: this.getSection, + getAllSections: this.getAllSections, + getSectionsEnabled: () => this.getAllSections().filter((section) => section.enabled), + }; + } +} diff --git a/src/plugins/management/public/management_service.test.ts b/src/plugins/management/public/management_service.test.ts deleted file mode 100644 index 1507d6f43619d..0000000000000 --- a/src/plugins/management/public/management_service.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ManagementService } from './management_service'; -import { ManagementSectionId } from './types'; -import { coreMock } from '../../../core/public/mocks'; -import { npSetup } from '../../../legacy/ui/public/new_platform/__mocks__'; - -jest.mock('ui/new_platform'); - -test('Provides default sections', () => { - const service = new ManagementService().setup( - npSetup.plugins.kibanaLegacy, - () => {}, - coreMock.createSetup().getStartServices - ); - expect(service.getAllSections().length).toEqual(6); - expect(service.getSection(ManagementSectionId.Ingest)).toBeDefined(); - expect(service.getSection(ManagementSectionId.Data)).toBeDefined(); - expect(service.getSection(ManagementSectionId.InsightsAndAlerting)).toBeDefined(); - expect(service.getSection(ManagementSectionId.Security)).toBeDefined(); - expect(service.getSection(ManagementSectionId.Kibana)).toBeDefined(); - expect(service.getSection(ManagementSectionId.Stack)).toBeDefined(); -}); diff --git a/src/plugins/management/public/management_service.ts b/src/plugins/management/public/management_service.ts deleted file mode 100644 index 84939fe095336..0000000000000 --- a/src/plugins/management/public/management_service.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ReactElement } from 'react'; - -import { ManagementSection } from './management_section'; -import { managementSections } from './management_sections'; -import { KibanaLegacySetup } from '../../kibana_legacy/public'; -// @ts-ignore -import { LegacyManagementSection, sections } from './legacy'; -import { CreateSection, ManagementSectionId } from './types'; -import { StartServicesAccessor, CoreStart } from '../../../core/public'; - -export class ManagementService { - private sections: ManagementSection[] = []; - - private register( - registerLegacyApp: KibanaLegacySetup['registerLegacyApp'], - getLegacyManagement: () => LegacyManagementSection, - getStartServices: StartServicesAccessor - ) { - return (section: CreateSection) => { - if (this.getSection(section.id)) { - throw Error(`ManagementSection '${section.id}' already registered`); - } - - const newSection = new ManagementSection( - section, - this.getSectionsEnabled.bind(this), - registerLegacyApp, - getLegacyManagement, - getStartServices - ); - this.sections.push(newSection); - return newSection; - }; - } - - private getSection(sectionId: ManagementSectionId) { - return this.sections.find((section) => section.id === sectionId); - } - - private getAllSections() { - return this.sections; - } - - private getSectionsEnabled() { - return this.sections - .filter((section) => section.getAppsEnabled().length > 0) - .sort((a, b) => a.order - b.order); - } - - private sharedInterface = { - getSection: (sectionId: ManagementSectionId) => { - const section = this.getSection(sectionId); - if (!section) { - throw new Error(`Management section with id ${sectionId} is undefined`); - } - return section; - }, - getSectionsEnabled: this.getSectionsEnabled.bind(this), - getAllSections: this.getAllSections.bind(this), - }; - - public setup( - kibanaLegacy: KibanaLegacySetup, - getLegacyManagement: () => LegacyManagementSection, - getStartServices: StartServicesAccessor - ) { - const register = this.register.bind(this)( - kibanaLegacy.registerLegacyApp, - getLegacyManagement, - getStartServices - ); - - managementSections.forEach( - ({ id, title }: { id: ManagementSectionId; title: ReactElement }, idx: number) => { - register({ id, title, order: idx }); - } - ); - - return { - ...this.sharedInterface, - }; - } - - public start(navigateToApp: CoreStart['application']['navigateToApp']) { - return { - navigateToApp, // apps are currently registered as top level apps but this may change in the future - ...this.sharedInterface, - }; - } -} diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 3e32ff4fe26b2..123e3f28877aa 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -18,29 +18,29 @@ */ import { ManagementSetup, ManagementStart } from '../types'; -import { ManagementSection } from '../management_section'; +import { ManagementSection } from '../index'; -const createManagementSectionMock = (): jest.Mocked> => { - return { +const createManagementSectionMock = () => + (({ + disable: jest.fn(), + enable: jest.fn(), registerApp: jest.fn(), getApp: jest.fn(), - getAppsEnabled: jest.fn().mockReturnValue([]), - }; -}; + getEnabledItems: jest.fn().mockReturnValue([]), + } as unknown) as ManagementSection); const createSetupContract = (): DeeplyMockedKeys => ({ sections: { + register: jest.fn(), getSection: jest.fn().mockReturnValue(createManagementSectionMock()), - getAllSections: jest.fn().mockReturnValue([]), }, }); const createStartContract = (): DeeplyMockedKeys => ({ - legacy: {}, sections: { getSection: jest.fn(), getAllSections: jest.fn(), - navigateToApp: jest.fn(), + getSectionsEnabled: jest.fn(), }, }); diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index e7f86996a9a1b..71656d7c0b83b 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -18,23 +18,30 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { ManagementSetup, ManagementStart } from './types'; -import { ManagementService } from './management_service'; -import { KibanaLegacySetup } from '../../kibana_legacy/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; -// @ts-ignore -import { LegacyManagementAdapter } from './legacy'; -import { showLegacyRedirectMessages } from './legacy/redirect_messages'; +import { + CoreSetup, + CoreStart, + Plugin, + DEFAULT_APP_CATEGORIES, + PluginInitializerContext, +} from '../../../core/public'; + +import { ManagementSectionsService } from './management_sections_service'; + +interface ManagementSetupDependencies { + home: HomePublicPluginSetup; +} export class ManagementPlugin implements Plugin { - private managementSections = new ManagementService(); - private legacyManagement = new LegacyManagementAdapter(); + private readonly managementSections = new ManagementSectionsService(); + + constructor(private initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, { home }: ManagementSetupDependencies) { + const kibanaVersion = this.initializerContext.env.packageInfo.version; - public setup( - core: CoreSetup, - { kibanaLegacy, home }: { kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup } - ) { home.featureCatalogue.register({ id: 'stack-management', title: i18n.translate('management.stackManagement.managementLabel', { @@ -44,25 +51,38 @@ export class ManagementPlugin implements Plugin ManagementSection; + getSection: (sectionId: ManagementSectionId | string) => ManagementSection; +} + +export interface SectionsServiceStart { + getSection: (sectionId: ManagementSectionId | string) => ManagementSection; + getAllSections: () => ManagementSection[]; + getSectionsEnabled: () => ManagementSection[]; } export enum ManagementSectionId { @@ -60,67 +50,20 @@ export enum ManagementSectionId { Stack = 'stack', } -interface SectionsServiceSetup { - getSection: (sectionId: ManagementSectionId) => ManagementSection; - getAllSections: () => ManagementSection[]; -} - -interface SectionsServiceStart { - getSection: (sectionId: ManagementSectionId) => ManagementSection; - getAllSections: () => ManagementSection[]; - navigateToApp: ApplicationStart['navigateToApp']; -} - -export interface CreateSection { - id: ManagementSectionId; - title: string | ReactElement; - order?: number; - euiIconType?: string; // takes precedence over `icon` property. - icon?: string; // URL to image file; fallback if no `euiIconType` -} - -export type RegisterSection = (section: CreateSection) => ManagementSection; - -export interface RegisterManagementAppArgs { - id: string; - title: string; - mount: ManagementSectionMount; - order?: number; -} - -export type RegisterManagementApp = (managementApp: RegisterManagementAppArgs) => ManagementApp; - export type Unmount = () => Promise | void; +export type Mount = (params: ManagementAppMountParams) => Unmount | Promise; export interface ManagementAppMountParams { basePath: string; // base path for setting up your router element: HTMLElement; // element the section should render into setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + history: ScopedHistory; } -export type ManagementSectionMount = ( - params: ManagementAppMountParams -) => Unmount | Promise; - -export interface CreateManagementApp { +export interface CreateManagementItemArgs { id: string; - title: string; - basePath: string; + title: string | ReactElement; order?: number; - mount: ManagementSectionMount; -} - -export interface LegacySection extends LegacyApp { - visibleItems: LegacyApp[]; -} - -export interface LegacyApp { - disabled: boolean; - visible: boolean; - id: string; - display: string; - url?: string; - euiIconType?: IconType; - icon?: string; - order: number; + euiIconType?: string; // takes precedence over `icon` property. + icon?: string; // URL to image file; fallback if no `euiIconType` } diff --git a/src/legacy/ui/public/management/breadcrumbs.ts b/src/plugins/management/public/utils/breadcrumbs.ts similarity index 85% rename from src/legacy/ui/public/management/breadcrumbs.ts rename to src/plugins/management/public/utils/breadcrumbs.ts index 936e99caff565..147d157d29d7f 100644 --- a/src/legacy/ui/public/management/breadcrumbs.ts +++ b/src/plugins/management/public/utils/breadcrumbs.ts @@ -19,9 +19,9 @@ import { i18n } from '@kbn/i18n'; -export const MANAGEMENT_BREADCRUMB = Object.freeze({ - text: i18n.translate('common.ui.stackManagement.breadcrumb', { +export const MANAGEMENT_BREADCRUMB = { + text: i18n.translate('management.breadcrumb', { defaultMessage: 'Stack Management', }), - href: '#/management', -}); + href: '/', +}; diff --git a/src/plugins/management/public/utils/index.ts b/src/plugins/management/public/utils/index.ts new file mode 100644 index 0000000000000..04c0c4c6811c7 --- /dev/null +++ b/src/plugins/management/public/utils/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { MANAGEMENT_BREADCRUMB } from './breadcrumbs'; +export { ManagementApp, RegisterManagementAppArgs } from './management_app'; +export { ManagementSection, RegisterManagementSectionArgs } from './management_section'; diff --git a/src/plugins/management/public/utils/management_app.ts b/src/plugins/management/public/utils/management_app.ts new file mode 100644 index 0000000000000..a27db5522af82 --- /dev/null +++ b/src/plugins/management/public/utils/management_app.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CreateManagementItemArgs, Mount } from '../types'; +import { ManagementItem } from './management_item'; + +export interface RegisterManagementAppArgs extends CreateManagementItemArgs { + mount: Mount; + basePath: string; +} + +export class ManagementApp extends ManagementItem { + public readonly mount: Mount; + public readonly basePath: string; + + constructor(args: RegisterManagementAppArgs) { + super(args); + + this.mount = args.mount; + this.basePath = args.basePath; + } +} diff --git a/src/plugins/management/public/utils/management_item.ts b/src/plugins/management/public/utils/management_item.ts new file mode 100644 index 0000000000000..ef0c8e4693895 --- /dev/null +++ b/src/plugins/management/public/utils/management_item.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ReactElement } from 'react'; +import { CreateManagementItemArgs } from '../types'; + +export class ManagementItem { + public readonly id: string = ''; + public readonly title: string | ReactElement = ''; + public readonly order: number; + public readonly euiIconType?: string; + public readonly icon?: string; + + public enabled: boolean = true; + + constructor({ id, title, order = 100, euiIconType, icon }: CreateManagementItemArgs) { + this.id = id; + this.title = title; + this.order = order; + this.euiIconType = euiIconType; + this.icon = icon; + } + + disable() { + this.enabled = false; + } + + enable() { + this.enabled = true; + } +} diff --git a/src/plugins/management/public/utils/management_section.test.ts b/src/plugins/management/public/utils/management_section.test.ts new file mode 100644 index 0000000000000..f5ce86f5dc963 --- /dev/null +++ b/src/plugins/management/public/utils/management_section.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ManagementSection, RegisterManagementSectionArgs } from './management_section'; + +describe('ManagementSection', () => { + const createSection = ( + config: RegisterManagementSectionArgs = { + id: 'test-section', + title: 'Test Section', + } as RegisterManagementSectionArgs + ) => new ManagementSection(config); + + test('cannot register two apps with the same id', () => { + const section = createSection(); + const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} }; + + section.registerApp(testAppConfig); + + expect(section.apps.length).toEqual(1); + + expect(() => { + section.registerApp(testAppConfig); + }).toThrow(); + }); + + test('can enable and disable apps', () => { + const section = createSection(); + const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} }; + + const app = section.registerApp(testAppConfig); + + expect(section.getAppsEnabled().length).toEqual(1); + + app.disable(); + + expect(section.getAppsEnabled().length).toEqual(0); + }); +}); diff --git a/src/plugins/management/public/utils/management_section.ts b/src/plugins/management/public/utils/management_section.ts new file mode 100644 index 0000000000000..d226825e39d19 --- /dev/null +++ b/src/plugins/management/public/utils/management_section.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Assign } from '@kbn/utility-types'; +import { CreateManagementItemArgs, ManagementSectionId } from '../types'; +import { ManagementItem } from './management_item'; +import { ManagementApp, RegisterManagementAppArgs } from './management_app'; + +export type RegisterManagementSectionArgs = Assign< + CreateManagementItemArgs, + { id: ManagementSectionId | string } +>; + +export class ManagementSection extends ManagementItem { + public readonly apps: ManagementApp[] = []; + + constructor(args: RegisterManagementSectionArgs) { + super(args); + } + + registerApp(args: Omit) { + if (this.getApp(args.id)) { + throw new Error(`Management app already registered - id: ${args.id}, title: ${args.title}`); + } + + const app = new ManagementApp({ + ...args, + basePath: `/${this.id}/${args.id}`, + }); + + this.apps.push(app); + + return app; + } + + getApp(id: ManagementApp['id']) { + return this.apps.find((app) => app.id === id); + } + + getAppsEnabled() { + return this.apps.filter((app) => app.enabled); + } +} diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index c1daf3445219f..9cfe99fd3bbf8 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -19,10 +19,10 @@ import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route } from 'react-router-dom'; +import { Router, Switch, Route } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { CoreSetup, Capabilities } from 'src/core/public'; +import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from '../../../management/public'; import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin'; import { ISavedObjectsManagementServiceRegistry } from '../services'; @@ -44,30 +44,41 @@ export const mountManagementSection = async ({ serviceRegistry, }: MountParams) => { const [coreStart, { data }, pluginStart] = await core.getStartServices(); - const { element, basePath, setBreadcrumbs } = mountParams; + const { element, history, setBreadcrumbs } = mountParams; if (allowedObjectTypes === undefined) { allowedObjectTypes = await getAllowedTypes(coreStart.http); } const capabilities = coreStart.application.capabilities; + const RedirectToHomeIfUnauthorized: React.FunctionComponent = ({ children }) => { + const allowed = capabilities?.management?.kibana?.objects ?? false; + + if (!allowed) { + coreStart.application.navigateToApp('home'); + return null; + } + return children! as React.ReactElement; + }; + ReactDOM.render( - + - + }> - + }> - + , element ); @@ -90,14 +101,3 @@ export const mountManagementSection = async ({ ReactDOM.unmountComponentAtNode(element); }; }; - -const RedirectToHomeIfUnauthorized: React.FunctionComponent<{ - capabilities: Capabilities; -}> = ({ children, capabilities }) => { - const allowed = capabilities?.management?.kibana?.objects ?? false; - if (!allowed) { - window.location.hash = '/home'; - return null; - } - return children! as React.ReactElement; -}; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx index 83644e6404c81..1572ef9164700 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx @@ -26,6 +26,7 @@ import { OverlayStart, NotificationsStart, SimpleSavedObject, + ScopedHistory, } from '../../../../../core/public'; import { ISavedObjectsManagementServiceRegistry } from '../../services'; import { Header, NotFoundErrors, Intro, Form } from './components'; @@ -41,6 +42,7 @@ interface SavedObjectEditionProps { notifications: NotificationsStart; notFoundType?: string; savedObjectsClient: SavedObjectsClientContract; + history: ScopedHistory; } interface SavedObjectEditionState { @@ -171,6 +173,6 @@ export class SavedObjectEdition extends Component< }; redirectToListing() { - window.location.hash = '/management/kibana/objects'; + this.props.history.push('/'); } } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 78af61c20c828..26ddb0809d24b 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -314,7 +314,7 @@ exports[`SavedObjectsTable relationships should show the flyout 1`] = ` Object { "id": "2", "meta": Object { - "editUrl": "#/management/kibana/objects/savedSearches/2", + "editUrl": "/management/kibana/objects/savedSearches/2", "icon": "search", "inAppUrl": Object { "path": "/discover/2", @@ -404,7 +404,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` Object { "id": "2", "meta": Object { - "editUrl": "#/management/kibana/objects/savedSearches/2", + "editUrl": "/management/kibana/objects/savedSearches/2", "icon": "search", "inAppUrl": Object { "path": "/discover/2", @@ -417,7 +417,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` Object { "id": "3", "meta": Object { - "editUrl": "#/management/kibana/objects/savedDashboards/3", + "editUrl": "/management/kibana/objects/savedDashboards/3", "icon": "dashboardApp", "inAppUrl": Object { "path": "/dashboard/3", @@ -430,7 +430,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` Object { "id": "4", "meta": Object { - "editUrl": "#/management/kibana/objects/savedVisualizations/4", + "editUrl": "/management/kibana/objects/savedVisualizations/4", "icon": "visualizeApp", "inAppUrl": Object { "path": "/edit/4", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap index cb2a2dd0e4d1e..6eb9e36394ee3 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap @@ -455,7 +455,7 @@ exports[`Relationships should render searches normally 1`] = ` "editUrl": "/management/kibana/indexPatterns/patterns/1", "icon": "indexPatternApp", "inAppUrl": Object { - "path": "/app/kibana#/management/kibana/indexPatterns/patterns/1", + "path": "/app/management/kibana/indexPatterns/patterns/1", "uiCapabilitiesPath": "management.kibana.index_patterns", }, "title": "My Index Pattern", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx index 5ee70e73c873b..9277d9b00305b 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -112,7 +112,7 @@ describe('Relationships', () => { editUrl: '/management/kibana/indexPatterns/patterns/1', icon: 'indexPatternApp', inAppUrl: { - path: '/app/kibana#/management/kibana/indexPatterns/patterns/1', + path: '/app/management/kibana/indexPatterns/patterns/1', uiCapabilitiesPath: 'management.kibana.index_patterns', }, title: 'My Index Pattern', @@ -141,7 +141,7 @@ describe('Relationships', () => { meta: { title: 'MySearch', icon: 'search', - editUrl: '#/management/kibana/objects/savedSearches/1', + editUrl: '/management/kibana/objects/savedSearches/1', inAppUrl: { path: '/discover/1', uiCapabilitiesPath: 'discover.show', @@ -208,7 +208,7 @@ describe('Relationships', () => { meta: { title: 'MyViz', icon: 'visualizeApp', - editUrl: '#/management/kibana/objects/savedVisualizations/1', + editUrl: '/management/kibana/objects/savedVisualizations/1', inAppUrl: { path: '/edit/1', uiCapabilitiesPath: 'visualize.show', @@ -275,7 +275,7 @@ describe('Relationships', () => { meta: { title: 'MyDashboard', icon: 'dashboardApp', - editUrl: '#/management/kibana/objects/savedDashboards/1', + editUrl: '/management/kibana/objects/savedDashboards/1', inAppUrl: { path: '/dashboard/1', uiCapabilitiesPath: 'dashboard.show', @@ -315,7 +315,7 @@ describe('Relationships', () => { meta: { title: 'MyDashboard', icon: 'dashboardApp', - editUrl: '#/management/kibana/objects/savedDashboards/1', + editUrl: '/management/kibana/objects/savedDashboards/1', inAppUrl: { path: '/dashboard/1', uiCapabilitiesPath: 'dashboard.show', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index b46bc8dd1b4ee..191bde8b192fd 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -168,7 +168,7 @@ describe('SavedObjectsTable', () => { meta: { title: `MySearch`, icon: 'search', - editUrl: '#/management/kibana/objects/savedSearches/2', + editUrl: '/management/kibana/objects/savedSearches/2', inAppUrl: { path: '/discover/2', uiCapabilitiesPath: 'discover.show', @@ -181,7 +181,7 @@ describe('SavedObjectsTable', () => { meta: { title: `MyDashboard`, icon: 'dashboardApp', - editUrl: '#/management/kibana/objects/savedDashboards/3', + editUrl: '/management/kibana/objects/savedDashboards/3', inAppUrl: { path: '/dashboard/3', uiCapabilitiesPath: 'dashboard.show', @@ -194,7 +194,7 @@ describe('SavedObjectsTable', () => { meta: { title: `MyViz`, icon: 'visualizeApp', - editUrl: '#/management/kibana/objects/savedVisualizations/4', + editUrl: '/management/kibana/objects/savedVisualizations/4', inAppUrl: { path: '/edit/4', uiCapabilitiesPath: 'visualize.show', @@ -441,7 +441,7 @@ describe('SavedObjectsTable', () => { meta: { title: `MySearch`, icon: 'search', - editUrl: '#/management/kibana/objects/savedSearches/2', + editUrl: '/management/kibana/objects/savedSearches/2', inAppUrl: { path: '/discover/2', uiCapabilitiesPath: 'discover.show', @@ -456,7 +456,7 @@ describe('SavedObjectsTable', () => { type: 'search', meta: { title: 'MySearch', - editUrl: '#/management/kibana/objects/savedSearches/2', + editUrl: '/management/kibana/objects/savedSearches/2', icon: 'search', inAppUrl: { path: '/discover/2', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index d9b856a79b496..c24f5d29f3870 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -457,8 +457,8 @@ export class SavedObjectsTable extends Component void; + history: ScopedHistory; }) => { const { service: serviceName, id } = useParams<{ service: string; id: string }>(); const capabilities = coreStart.application.capabilities; @@ -47,7 +49,7 @@ const SavedObjectsEditionPage = ({ text: i18n.translate('savedObjectsManagement.breadcrumb.index', { defaultMessage: 'Saved objects', }), - href: '#/management/kibana/objects', + href: '/', }, { text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { @@ -68,6 +70,7 @@ const SavedObjectsEditionPage = ({ notifications={coreStart.notifications} capabilities={capabilities} notFoundType={query.notFound as string} + history={history} /> ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index 4e8418d7406b5..75692777f08bb 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -52,7 +52,7 @@ const SavedObjectsTablePage = ({ text: i18n.translate('savedObjectsManagement.breadcrumb.index', { defaultMessage: 'Saved objects', }), - href: '#/management/kibana/objects', + href: '/', }, ]); }, [setBreadcrumbs]); @@ -73,11 +73,7 @@ const SavedObjectsTablePage = ({ goInspectObject={(savedObject) => { const { editUrl } = savedObject.meta; if (editUrl) { - // previously, kbnUrl.change(object.meta.editUrl); was used. - // using direct access to location.hash seems the only option for now, - // as using react-router-dom will prefix the url with the router's basename - // which should be ignored there. - window.location.hash = editUrl; + return coreStart.application.navigateToUrl('/app' + editUrl); } }} canGoInApp={(savedObject) => { diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index 1d765c70edb97..f3d6318db89f2 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -82,7 +82,7 @@ export class SavedObjectsManagementPlugin 'Import, export, and manage your saved searches, visualizations, and dashboards.', }), icon: 'savedObjectsApp', - path: '/app/kibana#/management/kibana/objects', + path: '/app/management/kibana/objects', showOnHomePage: true, category: FeatureCatalogueCategory.ADMIN, }); diff --git a/src/plugins/share/common/constants.ts b/src/plugins/share/common/constants.ts new file mode 100644 index 0000000000000..7ad8e39c279d3 --- /dev/null +++ b/src/plugins/share/common/constants.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const CSV_SEPARATOR_SETTING = 'csv:separator'; +export const CSV_QUOTE_VALUES_SETTING = 'csv:quoteValues'; diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 183219645467e..e3d6c41a278cd 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; + export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; export { SharePluginSetup, SharePluginStart } from './plugin'; diff --git a/src/plugins/share/public/url_generators/url_generator_service.ts b/src/plugins/share/public/url_generators/url_generator_service.ts index 13c1b94acdd07..b63e2a45d6812 100644 --- a/src/plugins/share/public/url_generators/url_generator_service.ts +++ b/src/plugins/share/public/url_generators/url_generator_service.ts @@ -24,7 +24,7 @@ import { UrlGeneratorInternal } from './url_generator_internal'; import { UrlGeneratorContract } from './url_generator_contract'; export interface UrlGeneratorsStart { - getUrlGenerator: (urlGeneratorId: UrlGeneratorId) => UrlGeneratorContract; + getUrlGenerator: (urlGeneratorId: T) => UrlGeneratorContract; } export interface UrlGeneratorsSetup { diff --git a/src/plugins/share/server/index.ts b/src/plugins/share/server/index.ts index 9e574314f8000..ff419ce68d46b 100644 --- a/src/plugins/share/server/index.ts +++ b/src/plugins/share/server/index.ts @@ -20,6 +20,8 @@ import { PluginInitializerContext } from '../../../core/server'; import { SharePlugin } from './plugin'; +export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; + export function plugin(initializerContext: PluginInitializerContext) { return new SharePlugin(initializerContext); } diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 0d9f183d13404..e444cb1658d95 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -17,9 +17,12 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { createRoutes } from './routes/create_routes'; import { url } from './saved_objects'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../common/constants'; export class SharePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -27,6 +30,28 @@ export class SharePlugin implements Plugin { public async setup(core: CoreSetup) { createRoutes(core, this.initializerContext.logger.get()); core.savedObjects.registerType(url); + core.uiSettings.register({ + [CSV_SEPARATOR_SETTING]: { + name: i18n.translate('share.advancedSettings.csv.separatorTitle', { + defaultMessage: 'CSV separator', + }), + value: ',', + description: i18n.translate('share.advancedSettings.csv.separatorText', { + defaultMessage: 'Separate exported values with this string', + }), + schema: schema.string(), + }, + [CSV_QUOTE_VALUES_SETTING]: { + name: i18n.translate('share.advancedSettings.csv.quoteValuesTitle', { + defaultMessage: 'Quote CSV values', + }), + value: true, + description: i18n.translate('share.advancedSettings.csv.quoteValuesText', { + defaultMessage: 'Should values be quoted in csv exports?', + }), + schema: schema.boolean(), + }, + }); } public start() { diff --git a/src/plugins/telemetry/common/constants.ts b/src/plugins/telemetry/common/constants.ts index 2e058fdc13e4a..53c79b738f750 100644 --- a/src/plugins/telemetry/common/constants.ts +++ b/src/plugins/telemetry/common/constants.ts @@ -49,7 +49,7 @@ export const LOCALSTORAGE_KEY = 'telemetry.data'; /** * Link to Advanced Settings. */ -export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings'; +export const PATH_TO_ADVANCED_SETTINGS = 'management/kibana/settings'; /** * Link to the Elastic Telemetry privacy statement. diff --git a/src/plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap index 193205cd394e2..dd774d956dc10 100644 --- a/src/plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap +++ b/src/plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap @@ -10,7 +10,7 @@ exports[`OptInDetailsComponent renders as expected 1`] = ` values={ Object { "disableLink": ) { setValue(value && agg.params.min_doc_count); } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [agg.params.min_doc_count, setValue, value]); return ( diff --git a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx index 02bf680734526..0d21eb04c12b2 100644 --- a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx @@ -23,6 +23,7 @@ import React, { useEffect, useCallback } from 'react'; import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { UI_SETTINGS } from '../../../../data/public'; import { AggParamEditorProps } from '../agg_param_props'; @@ -38,7 +39,7 @@ const label = ( } type="questionInCircle" diff --git a/src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts b/src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts index ee24e2b42113d..950c856349230 100644 --- a/src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts +++ b/src/plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts @@ -33,6 +33,7 @@ const CUSTOM_METRIC = { }; function useCompatibleAggCallback(aggFilter: AggFilter) { + /* eslint-disable-next-line react-hooks/exhaustive-deps */ return useCallback(isCompatibleAggregation(aggFilter), [aggFilter]); } diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js index f67dcf42adff6..bd7626a493338 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.js @@ -17,6 +17,7 @@ * under the License. */ import _ from 'lodash'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; import aggTableTemplate from './agg_table.html'; import { getFormatService } from '../services'; import { i18n } from '@kbn/i18n'; @@ -47,8 +48,8 @@ export function KbnAggTable(config, RecursionHelper) { self._saveAs = require('@elastic/filesaver').saveAs; self.csv = { - separator: config.get('csv:separator'), - quoteValues: config.get('csv:quoteValues'), + separator: config.get(CSV_SEPARATOR_SETTING), + quoteValues: config.get(CSV_QUOTE_VALUES_SETTING), }; self.exportAsCsv = function (formatted) { diff --git a/src/plugins/vis_type_timelion/public/components/panel.tsx b/src/plugins/vis_type_timelion/public/components/panel.tsx index 4c28e4e5a18ab..99c5532c04832 100644 --- a/src/plugins/vis_type_timelion/public/components/panel.tsx +++ b/src/plugins/vis_type_timelion/public/components/panel.tsx @@ -102,6 +102,7 @@ function Panel({ interval, seriesList, renderComplete }: PanelProps) { [chartElem] ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const highlightSeries = useCallback( debounce(({ currentTarget }: JQuery.TriggeredEvent) => { const id = Number(currentTarget.getAttribute(SERIES_ID_ATTR)); @@ -295,6 +296,7 @@ function Panel({ interval, seriesList, renderComplete }: PanelProps) { [plot, legendValueNumbers, unhighlightSeries, legendCaption] ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const debouncedSetLegendNumbers = useCallback( debounce(setLegendNumbers, DEBOUNCE_DELAY, { maxWait: DEBOUNCE_DELAY, diff --git a/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx index 8c76b41df0ced..8258b92b096c1 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx @@ -20,7 +20,7 @@ import React, { useEffect, useCallback, useRef, useMemo } from 'react'; import { EuiFormLabel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { monaco } from '@kbn/ui-shared-deps/monaco'; +import { monaco } from '@kbn/monaco'; import { CodeEditor, useKibana } from '../../../kibana_react/public'; import { suggest, getSuggestion } from './timelion_expression_input_helpers'; diff --git a/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts index f7b3433105b76..8db057cdb3cc5 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts @@ -19,7 +19,7 @@ import { get, startsWith } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { monaco } from '@kbn/ui-shared-deps/monaco'; +import { monaco } from '@kbn/monaco'; import { Parser } from 'pegjs'; diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index 1bc979399b1b1..a624ff72ead69 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -30,6 +30,7 @@ import _ from 'lodash'; import { expect } from 'chai'; import sinon from 'sinon'; import invoke from '../helpers/invoke_series_fn.js'; +import { UI_SETTINGS } from '../../../../data/server'; function stubRequestAndServer(response, indexPatternSavedObjects = []) { return { @@ -216,14 +217,14 @@ describe('es', () => { it('sets ignore_throttled=true on the request', () => { config.index = 'beer'; - tlConfig.settings['search:includeFrozen'] = false; + tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = false; const request = fn(config, tlConfig, emptyScriptedFields); expect(request.ignore_throttled).to.equal(true); }); it('sets no timeout if elasticsearch.shardTimeout is set to 0', () => { - tlConfig.settings['search:includeFrozen'] = true; + tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = true; config.index = 'beer'; const request = fn(config, tlConfig, emptyScriptedFields); diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js index 65b28fb833279..bc0e368fbdab1 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js @@ -21,6 +21,7 @@ import _ from 'lodash'; import moment from 'moment'; import { buildAggBody } from './agg_body'; import createDateAgg from './create_date_agg'; +import { UI_SETTINGS } from '../../../../../data/server'; export default function buildRequest(config, tlConfig, scriptedFields, timeout) { const bool = { must: [] }; @@ -78,7 +79,7 @@ export default function buildRequest(config, tlConfig, scriptedFields, timeout) const request = { index: config.index, - ignore_throttled: !tlConfig.settings['search:includeFrozen'], + ignore_throttled: !tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN], body: { query: { bool: bool, diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js index 972f937ad109d..84da28718e323 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js @@ -18,7 +18,8 @@ */ import { getUISettings } from '../../../services'; +import { UI_SETTINGS } from '../../../../../data/public'; export function getDefaultQueryLanguage() { - return getUISettings().get('search:queryLanguage'); + return getUISettings().get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js index 71e82770bfa03..308579126eeb1 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js @@ -20,6 +20,7 @@ import { createTickFormatter } from './tick_formatter'; import { getFieldFormatsRegistry } from '../../../../../../test_utils/public/stub_field_formats'; import { setFieldFormats } from '../../../services'; +import { UI_SETTINGS } from '../../../../../data/public'; const mockUiSettings = { get: (item) => { @@ -28,11 +29,11 @@ const mockUiSettings = { getUpdate$: () => ({ subscribe: jest.fn(), }), - 'query:allowLeadingWildcards': true, - 'query:queryString:options': {}, - 'courier:ignoreFilterIfFieldNotInIndex': true, + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: true, + [UI_SETTINGS.QUERY_STRING_OPTIONS]: {}, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, 'dateFormat:tz': 'Browser', - 'format:defaultTypeMap': {}, + [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, }; const mockCore = { @@ -55,7 +56,7 @@ describe('createTickFormatter(format, template)', () => { test('returns a percent with percent formatter', () => { const config = { - 'format:percent:defaultPattern': '0.[00]%', + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0.[00]%', }; const fn = createTickFormatter('percent', null, (key) => config[key]); expect(fn(0.5556)).toEqual('55.56%'); @@ -63,7 +64,7 @@ describe('createTickFormatter(format, template)', () => { test('returns a byte formatted string with byte formatter', () => { const config = { - 'format:bytes:defaultPattern': '0.0b', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0.0b', }; const fn = createTickFormatter('bytes', null, (key) => config[key]); expect(fn(1500 ^ 10)).toEqual('1.5KB'); @@ -76,7 +77,7 @@ describe('createTickFormatter(format, template)', () => { test('returns a located string with custom locale setting', () => { const config = { - 'format:number:defaultLocale': 'fr', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'fr', }; const fn = createTickFormatter('0,0.0', null, (key) => config[key]); expect(fn(1500)).toEqual('1 500,0'); @@ -99,7 +100,7 @@ describe('createTickFormatter(format, template)', () => { test('returns formatted value if passed a bad template', () => { const config = { - 'format:number:defaultPattern': '0,0.[00]', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[00]', }; const fn = createTickFormatter('number', '{{value', (key) => config[key]); expect(fn(1.5556)).toEqual('1.56'); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 0c53ddd3f0ba8..a96890d4d1502 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -29,7 +29,7 @@ import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; -import { esKuery } from '../../../../../plugins/data/public'; +import { esKuery, UI_SETTINGS } from '../../../../../plugins/data/public'; import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; @@ -89,7 +89,9 @@ export class VisEditor extends Component { isValidKueryQuery = (filterQuery) => { if (filterQuery && filterQuery.language === 'kuery') { try { - const queryOptions = this.coreContext.uiSettings.get('query:allowLeadingWildcards'); + const queryOptions = this.coreContext.uiSettings.get( + UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS + ); esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); } catch (error) { return false; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js index 93a4eaba4ad9e..9ada39e359589 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js @@ -17,12 +17,15 @@ * under the License. */ import { AbstractSearchRequest } from './abstract_request'; +import { UI_SETTINGS } from '../../../../../data/server'; const SEARCH_METHOD = 'msearch'; export class MultiSearchRequest extends AbstractSearchRequest { async search(searches) { - const includeFrozen = await this.req.getUiSettingsService().get('search:includeFrozen'); + const includeFrozen = await this.req + .getUiSettingsService() + .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); const multiSearchBody = searches.reduce( (acc, { body, index }) => [ ...acc, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js index 1e28965a35793..c113db76332b7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js @@ -17,6 +17,7 @@ * under the License. */ import { MultiSearchRequest } from './multi_search_request'; +import { UI_SETTINGS } from '../../../../../data/server'; describe('MultiSearchRequest', () => { let searchRequest; @@ -51,7 +52,7 @@ describe('MultiSearchRequest', () => { expect(responses).toEqual([]); expect(req.getUiSettingsService).toHaveBeenCalled(); - expect(getServiceMock).toHaveBeenCalledWith('search:includeFrozen'); + expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); expect(callWithRequest).toHaveBeenCalledWith(req, 'msearch', { body: [ { ignoreUnavailable: true, index: 'index' }, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js index 110deb6a9bc1b..7d8b60a7e4595 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js @@ -17,12 +17,15 @@ * under the License. */ import { AbstractSearchRequest } from './abstract_request'; +import { UI_SETTINGS } from '../../../../../data/server'; const SEARCH_METHOD = 'search'; export class SingleSearchRequest extends AbstractSearchRequest { async search([{ body, index }]) { - const includeFrozen = await this.req.getUiSettingsService().get('search:includeFrozen'); + const includeFrozen = await this.req + .getUiSettingsService() + .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); const resp = await this.callWithRequest(this.req, SEARCH_METHOD, { ignore_throttled: !includeFrozen, body, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js index 043bd52d87aad..b899814f2fe13 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js @@ -17,6 +17,7 @@ * under the License. */ import { SingleSearchRequest } from './single_search_request'; +import { UI_SETTINGS } from '../../../../../data/server'; describe('SingleSearchRequest', () => { let searchRequest; @@ -48,7 +49,7 @@ describe('SingleSearchRequest', () => { expect(responses).toEqual([{}]); expect(req.getUiSettingsService).toHaveBeenCalled(); - expect(getServiceMock).toHaveBeenCalledWith('search:includeFrozen'); + expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); expect(callWithRequest).toHaveBeenCalledWith(req, 'search', { body: 'body', index: 'index', diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js index 42b8681f142e0..b427e5f12cadc 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js @@ -17,12 +17,14 @@ * under the License. */ +import { UI_SETTINGS } from '../../../../../data/server'; + export async function getEsQueryConfig(req) { const uiSettings = req.getUiSettingsService(); - const allowLeadingWildcards = await uiSettings.get('query:allowLeadingWildcards'); - const queryStringOptions = await uiSettings.get('query:queryString:options'); + const allowLeadingWildcards = await uiSettings.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS); + const queryStringOptions = await uiSettings.get(UI_SETTINGS.QUERY_STRING_OPTIONS); const ignoreFilterIfFieldNotInIndex = await uiSettings.get( - 'courier:ignoreFilterIfFieldNotInIndex' + UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX ); return { allowLeadingWildcards, diff --git a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js index 9d8f9dccb1f0a..d2cf81a1410c6 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js +++ b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js @@ -19,7 +19,7 @@ import React from 'react'; import _ from 'lodash'; -import numeral from 'numeral'; +import numeral from '@elastic/numeral'; import { renderToStaticMarkup } from 'react-dom/server'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js index 8e3429a39c955..938d3d0ec6d74 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js @@ -20,7 +20,7 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import numeral from 'numeral'; +import numeral from '@elastic/numeral'; import { PieContainsAllZeros, ContainerTooSmall } from '../errors'; import { Chart } from './_chart'; import { truncateLabel } from '../components/labels/truncate_labels'; diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts new file mode 100644 index 0000000000000..9129f060c5eef --- /dev/null +++ b/src/plugins/visualizations/common/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs'; diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 81794c31527ad..45c750de05ae1 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -29,6 +29,7 @@ import { getCapabilities, } from '../services'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, @@ -44,7 +45,7 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe const editUrl = visId ? getHttp().basePath.prepend(`/app/visualize${savedVisualizations.urlFor(visId)}`) : ''; - const isLabsEnabled = getUISettings().get('visualize:enableLabs'); + const isLabsEnabled = getUISettings().get(VISUALIZE_ENABLE_LABS_SETTING); if (!isLabsEnabled && vis.type.stage === 'experimental') { return new DisabledLabEmbeddable(vis.title, input); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index c4aa4c262edb0..c4267c9a36f78 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -43,6 +43,7 @@ import { convertToSerializedVis } from '../saved_visualizations/_saved_vis'; import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; @@ -82,7 +83,7 @@ export class VisualizeEmbeddableFactory if (!visType) { return false; } - if (getUISettings().get('visualize:enableLabs')) { + if (getUISettings().get(VISUALIZE_ENABLE_LABS_SETTING)) { return true; } return visType.stage !== 'experimental'; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index e475684ed5934..0bbf862216ed5 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import './index.scss'; - import { PublicContract } from '@kbn/utility-types'; import { PluginInitializerContext } from 'src/core/public'; import { VisualizationsPlugin, VisualizationsSetup, VisualizationsStart } from './plugin'; @@ -53,3 +51,4 @@ export { VisSavedObject, VisResponseValue, } from './types'; +export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 70c3bc2c1ed05..05644eddc5fca 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -66,6 +66,7 @@ const createInstance = async () => { inspector: inspectorPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index ef64eccfea31f..3546fa4056491 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext, CoreSetup, @@ -45,6 +47,7 @@ import { setChrome, setOverlays, setSavedSearchLoader, + setEmbeddable, } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, @@ -52,7 +55,7 @@ import { createVisEmbeddableFromObject, } from './embeddable'; import { ExpressionsSetup, ExpressionsStart } from '../../expressions/public'; -import { EmbeddableSetup } from '../../embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { range as rangeExpressionFunction } from './expression_functions/range'; @@ -102,6 +105,7 @@ export interface VisualizationsSetupDeps { export interface VisualizationsStartDeps { data: DataPublicPluginStart; expressions: ExpressionsStart; + embeddable: EmbeddableStart; inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; @@ -151,11 +155,12 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); setTypes(types); + setEmbeddable(embeddable); setApplication(core.application); setCapabilities(core.application.capabilities); setHttp(core.http); diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index 15055022af8aa..0761b8862e8e3 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -40,6 +40,7 @@ import { ExpressionsStart } from '../../../plugins/expressions/public'; import { UiActionsStart } from '../../../plugins/ui_actions/public'; import { SavedVisualizationsLoader } from './saved_visualizations'; import { SavedObjectLoader } from '../../saved_objects/public'; +import { EmbeddableStart } from '../../embeddable/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -49,6 +50,8 @@ export const [getHttp, setHttp] = createGetterSetter('Http'); export const [getApplication, setApplication] = createGetterSetter('Application'); +export const [getEmbeddable, setEmbeddable] = createGetterSetter('Embeddable'); + export const [getSavedObjects, setSavedObjects] = createGetterSetter( 'SavedObjects' ); diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index cea92b1db93aa..1a970e505b7c7 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -29,6 +29,7 @@ import { TypeSelection } from './type_selection'; import { TypesStart, VisType, VisTypeAlias } from '../vis_types'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; import { EMBEDDABLE_ORIGINATING_APP_PARAM } from '../../../embeddable/public'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; interface TypeSelectionProps { isOpen: boolean; @@ -65,7 +66,7 @@ class NewVisModal extends React.Component { { id: '2', schema: 'split', - params: { foo: 'bar', row: true }, + params: { foo: 'bar' }, }, { id: '3', schema: 'split', - params: { hey: 'ya', row: false }, + params: { hey: 'ya' }, }, ]; const tableDoc = generateDoc('table', aggs); @@ -656,7 +656,7 @@ describe('migration visualization', () => { { id: '2', schema: 'split', - params: { foo: 'bar', row: true }, + params: { foo: 'bar' }, }, { id: '3', @@ -681,7 +681,7 @@ describe('migration visualization', () => { { id: '2', schema: 'split', - params: { foo: 'bar', row: true }, + params: { foo: 'bar' }, }, ]; const tableDoc = generateDoc('table', aggs); @@ -701,12 +701,12 @@ describe('migration visualization', () => { { id: '2', schema: 'split', - params: { foo: 'bar', row: true }, + params: { foo: 'bar' }, }, { id: '3', schema: 'split', - params: { hey: 'ya', row: false }, + params: { hey: 'ya' }, }, { id: '4', @@ -731,15 +731,15 @@ describe('migration visualization', () => { { id: '2', schema: 'split', - params: { foo: 'bar', row: true }, + params: { foo: 'bar' }, }, { id: '3', schema: 'split', - params: { hey: 'ya', row: false }, + params: { hey: 'ya' }, }, ]; - const expected = [{}, { foo: 'bar', row: true }, { hey: 'ya' }]; + const expected = [{}, { foo: 'bar' }, { hey: 'ya' }]; const migrated = migrate(generateDoc('table', aggs)); const actual = JSON.parse(migrated.attributes.visState); @@ -1386,11 +1386,11 @@ describe('migration visualization', () => { doc as Parameters[0], savedObjectMigrationContext ); - const generateDoc = (params: any) => ({ + const generateDoc = (visState: any) => ({ attributes: { title: 'My Vis', description: 'This is my super cool vis.', - visState: JSON.stringify({ params }), + visState: JSON.stringify(visState), uiStateJSON: '{}', version: 1, kibanaSavedObjectMeta: { @@ -1416,7 +1416,7 @@ describe('migration visualization', () => { }, ], }; - const timeSeriesDoc = generateDoc(params); + const timeSeriesDoc = generateDoc({ params }); const migratedtimeSeriesDoc = migrate(timeSeriesDoc); const migratedParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; @@ -1453,12 +1453,38 @@ describe('migration visualization', () => { }, ], }; - const timeSeriesDoc = generateDoc(params); + const timeSeriesDoc = generateDoc({ params }); const migratedtimeSeriesDoc = migrate(timeSeriesDoc); const migratedParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; expect(migratedParams.gauge_color_rules[1]).toEqual(params.gauge_color_rules[1]); }); + + it('should move "row" field on split chart by a row or column to vis.params', () => { + const visData = { + type: 'area', + aggs: [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + type: 'terms', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + ], + params: {}, + }; + + const migrated = migrate(generateDoc(visData)); + const actual = JSON.parse(migrated.attributes.visState); + + expect(actual.aggs.filter((agg: any) => 'row' in agg.params)).toEqual([]); + expect(actual.params.row).toBeTruthy(); + }); }); describe('7.8.0 tsvb split_color_mode', () => { diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 71b286cd91a54..27fe722019a27 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -131,6 +131,50 @@ const migrateOperatorKeyTypo: SavedObjectMigrationFn = (doc) => { return doc; }; +/** + * Moving setting wether to do a row or column split to vis.params + * + * @see https://github.com/elastic/kibana/pull/58462/files#diff-ae69fe15b20a5099d038e9bbe2ed3849 + **/ +const migrateSplitByChartRow: SavedObjectMigrationFn = (doc) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState: any; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + + if (visState && visState.aggs && visState.params) { + let row: boolean | undefined; + + visState.aggs.forEach((agg: any) => { + if (agg.type === 'terms' && agg.schema === 'split' && 'row' in agg.params) { + row = agg.params.row; + + delete agg.params.row; + } + }); + + if (row !== undefined) { + visState.params.row = row; + } + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + + return doc; +}; + // Migrate date histogram aggregation (remove customInterval) const migrateDateHistogramAggregation: SavedObjectMigrationFn = (doc) => { const visStateJSON = get(doc, 'attributes.visState'); @@ -673,6 +717,6 @@ export const visualizationSavedObjectTypeMigrations = { ), '7.3.1': flow>(migrateFiltersAggQueryStringQueries), '7.4.2': flow>(transformSplitFiltersStringToQueryObject), - '7.7.0': flow>(migrateOperatorKeyTypo), + '7.7.0': flow>(migrateOperatorKeyTypo, migrateSplitByChartRow), '7.8.0': flow>(migrateTsvbDefaultColorPalettes), }; diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index e6bd4d94be09e..817a8262f9198 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -11,8 +11,5 @@ "visualizations", "dashboard" ], - "optionalPlugins": [ - "home", - "share" - ] + "optionalPlugins": ["home", "share"] } diff --git a/src/plugins/visualize/public/application/editor/editor.js b/src/plugins/visualize/public/application/editor/editor.js index 8e5ab5251015d..0e341ebf46e3a 100644 --- a/src/plugins/visualize/public/application/editor/editor.js +++ b/src/plugins/visualize/public/application/editor/editor.js @@ -40,7 +40,12 @@ import { migrateLegacyQuery, } from '../../../../kibana_legacy/public'; import { showSaveModal, SavedObjectSaveModalOrigin } from '../../../../saved_objects/public'; -import { esFilters, connectToQueryState, syncQueryStateWithUrl } from '../../../../data/public'; +import { + esFilters, + connectToQueryState, + syncQueryStateWithUrl, + UI_SETTINGS, +} from '../../../../data/public'; import { initVisEditorDirective } from './visualization_editor'; import { initVisualizationDirective } from './visualization'; @@ -109,12 +114,12 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState const defaultQuery = { query: '', language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || + uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }; const originatingApp = $route.current.params[EMBEDDABLE_ORIGINATING_APP_PARAM]; removeQueryParam(history, EMBEDDABLE_ORIGINATING_APP_PARAM); - $scope.getOriginatingApp = () => originatingApp; const visStateToEditorState = () => { diff --git a/src/plugins/visualize/public/application/legacy_app.js b/src/plugins/visualize/public/application/legacy_app.js index c9ccd79ce6a8a..24316f373a59e 100644 --- a/src/plugins/visualize/public/application/legacy_app.js +++ b/src/plugins/visualize/public/application/legacy_app.js @@ -211,22 +211,16 @@ export function initVisualizeApp(app, deps) { mapping: { visualization: VisualizeConstants.LANDING_PAGE_PATH, search: { - app: 'kibana', - path: - '#/management/kibana/objects/savedVisualizations/' + - $route.current.params.id, + app: 'management', + path: 'kibana/objects/savedVisualizations/' + $route.current.params.id, }, 'index-pattern': { - app: 'kibana', - path: - '#/management/kibana/objects/savedVisualizations/' + - $route.current.params.id, + app: 'management', + path: 'kibana/objects/savedVisualizations/' + $route.current.params.id, }, 'index-pattern-field': { - app: 'kibana', - path: - '#/management/kibana/objects/savedVisualizations/' + - $route.current.params.id, + app: 'management', + path: 'kibana/objects/savedVisualizations/' + $route.current.params.id, }, }, toastNotifications, diff --git a/src/plugins/visualize/public/application/listing/visualize_listing.js b/src/plugins/visualize/public/application/listing/visualize_listing.js index 228cfa1e9e492..e8e8d92034113 100644 --- a/src/plugins/visualize/public/application/listing/visualize_listing.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing.js @@ -25,6 +25,8 @@ import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; import { syncQueryStateWithUrl } from '../../../../data/public'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../visualizations/public'; + import { EuiLink } from '@elastic/eui'; import React from 'react'; @@ -120,7 +122,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor } this.fetchItems = (filter) => { - const isLabsEnabled = uiSettings.get('visualize:enableLabs'); + const isLabsEnabled = uiSettings.get(VISUALIZE_ENABLE_LABS_SETTING); return savedVisualizations .findListItems(filter, savedObjectsPublic.settings.getListingLimit()) .then((result) => { diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 115399cb57dfa..e15a9e989d21f 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -285,7 +285,7 @@ export default function ({ getService }: FtrProviderContext) { '/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', inAppUrl: { path: - '/app/kibana#/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', + '/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.index_patterns', }, }); diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index bbc691dbc6398..1db4df181e0e9 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -86,7 +86,7 @@ export default function ({ getService }: FtrProviderContext) { '/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', inAppUrl: { path: - '/app/kibana#/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', + '/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.index_patterns', }, }, @@ -127,7 +127,7 @@ export default function ({ getService }: FtrProviderContext) { '/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', inAppUrl: { path: - '/app/kibana#/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', + '/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.index_patterns', }, }, diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index e72ded3a6f2fd..c0abf236e3412 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -71,7 +71,7 @@ export async function createTestUserService( } } - async setRoles(roles: string[]) { + async setRoles(roles: string[], shouldRefreshBrowser: boolean = true) { if (isEnabled()) { log.debug(`set roles = ${roles}`); await user.create('test_user', { @@ -80,7 +80,7 @@ export async function createTestUserService( full_name: 'test user', }); - if (browser && testSubjects) { + if (browser && testSubjects && shouldRefreshBrowser) { if (await testSubjects.exists('kibanaChrome', { allowHidden: true })) { await browser.refresh(); await testSubjects.find('kibanaChrome', config.get('timeouts.find') * 10); diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index 503517a98c69e..ead6412564751 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -25,7 +25,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('bundle compression', function () { - this.tags('ciGroup12'); + this.tags(['ciGroup12', 'skipCoverage']); let buildNum; before(async () => { diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index 470ef462b9d9d..66888d441954e 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -17,8 +17,6 @@ * under the License. */ -import expect from '@kbn/expect'; - const TEST_INDEX_PATTERN = 'logstash-*'; const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI'; const TEST_ANCHOR_FILTER_FIELD = 'geo.src'; @@ -40,20 +38,19 @@ export default function ({ getService, getPageObjects }) { }); it('inclusive filter should be addable via expanded doc table rows', async function () { - await docTable.toggleRowExpanded({ isAnchorRow: true }); - - await retry.try(async () => { + await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => { + await docTable.toggleRowExpanded({ isAnchorRow: true }); const anchorDetailsRow = await docTable.getAnchorDetailsRow(); await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - expect( - await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true) - ).to.be(true); + + return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true); + }); + await retry.waitFor(`filter matching docs in docTable`, async () => { const fields = await docTable.getFields(); - const hasOnlyFilteredRows = fields + return fields .map((row) => row[2]) .every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); - expect(hasOnlyFilteredRows).to.be(true); }); }); @@ -64,26 +61,27 @@ export default function ({ getService, getPageObjects }) { await filterBar.toggleFilterEnabled(TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - await retry.try(async () => { - expect( - await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false) - ).to.be(true); + await retry.waitFor(`a disabled filter in filterbar`, async () => { + return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false); + }); + + await retry.waitFor('filters are disabled', async () => { const fields = await docTable.getFields(); const hasOnlyFilteredRows = fields .map((row) => row[2]) .every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); - expect(hasOnlyFilteredRows).to.be(false); + return hasOnlyFilteredRows === false; }); }); it('filter for presence should be addable via expanded doc table rows', async function () { await docTable.toggleRowExpanded({ isAnchorRow: true }); - await retry.try(async () => { + await retry.waitFor('an exists filter in the filterbar', async () => { const anchorDetailsRow = await docTable.getAnchorDetailsRow(); await docTable.addExistsFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true)).to.be(true); + return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true); }); }); }); diff --git a/test/functional/apps/context/_size.js b/test/functional/apps/context/_size.js index 3beb070b50deb..067a23daacb4a 100644 --- a/test/functional/apps/context/_size.js +++ b/test/functional/apps/context/_size.js @@ -16,69 +16,69 @@ * specific language governing permissions and limitations * under the License. */ - -import expect from '@kbn/expect'; - const TEST_INDEX_PATTERN = 'logstash-*'; const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI'; -const TEST_DEFAULT_CONTEXT_SIZE = 7; -const TEST_STEP_SIZE = 3; +const TEST_DEFAULT_CONTEXT_SIZE = 2; +const TEST_STEP_SIZE = 2; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const docTable = getService('docTable'); const PageObjects = getPageObjects(['context']); + let expectedRowLength = 2 * TEST_DEFAULT_CONTEXT_SIZE + 1; - // FLAKY: https://github.com/elastic/kibana/issues/53888 - describe.skip('context size', function contextSize() { + describe('context size', function contextSize() { before(async function () { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, }); + await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); }); it('should default to the `context:defaultSize` setting', async function () { - await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); - - await retry.try(async function () { - expect(await docTable.getRowsText()).to.have.length(2 * TEST_DEFAULT_CONTEXT_SIZE + 1); - }); - await retry.try(async function () { - const predecessorCountPicker = await PageObjects.context.getPredecessorCountPicker(); - expect(await predecessorCountPicker.getAttribute('value')).to.equal( - `${TEST_DEFAULT_CONTEXT_SIZE}` - ); - }); - await retry.try(async function () { - const successorCountPicker = await PageObjects.context.getSuccessorCountPicker(); - expect(await successorCountPicker.getAttribute('value')).to.equal( - `${TEST_DEFAULT_CONTEXT_SIZE}` - ); - }); + await retry.waitFor( + `number of rows displayed initially is ${expectedRowLength}`, + async function () { + const rows = await docTable.getRowsText(); + return rows.length === expectedRowLength; + } + ); + await retry.waitFor( + `predecessor count picker is set to ${TEST_DEFAULT_CONTEXT_SIZE}`, + async function () { + const predecessorCountPicker = await PageObjects.context.getPredecessorCountPicker(); + const value = await predecessorCountPicker.getAttribute('value'); + return value === String(TEST_DEFAULT_CONTEXT_SIZE); + } + ); }); it('should increase according to the `context:step` setting when clicking the `load newer` button', async function () { - await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); await PageObjects.context.clickPredecessorLoadMoreButton(); + expectedRowLength += TEST_STEP_SIZE; - await retry.try(async function () { - expect(await docTable.getRowsText()).to.have.length( - 2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1 - ); - }); + await retry.waitFor( + `number of rows displayed after clicking load more predecessors is ${expectedRowLength}`, + async function () { + const rows = await docTable.getRowsText(); + return rows.length === expectedRowLength; + } + ); }); it('should increase according to the `context:step` setting when clicking the `load older` button', async function () { - await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); await PageObjects.context.clickSuccessorLoadMoreButton(); + expectedRowLength += TEST_STEP_SIZE; - await retry.try(async function () { - expect(await docTable.getRowsText()).to.have.length( - 2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1 - ); - }); + await retry.waitFor( + `number of rows displayed after clicking load more successors is ${expectedRowLength}`, + async function () { + const rows = await docTable.getRowsText(); + return rows.length === expectedRowLength; + } + ); }); }); } diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js index ba715f3472b98..f5c2496a9a5aa 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/create_and_add_embeddables.js @@ -20,6 +20,7 @@ import expect from '@kbn/expect'; import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualizations/common/constants'; export default function ({ getService, getPageObjects }) { const retry = getService('retry'); @@ -102,7 +103,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs'); + await PageObjects.settings.toggleAdvancedSettingCheckbox(VISUALIZE_ENABLE_LABS_SETTING); }); it('should not display lab visualizations in add panel', async () => { @@ -117,7 +118,7 @@ export default function ({ getService, getPageObjects }) { after(async () => { await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs'); + await PageObjects.settings.clearAdvancedSettings(VISUALIZE_ENABLE_LABS_SETTING); await PageObjects.header.clickDashboard(); }); }); diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 6bc34a8b998a4..c931e6763f483 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -217,6 +217,11 @@ export default function ({ getService, getPageObjects }) { const hasWarningFieldFilter = await filterBar.hasFilter('extension', 'warn', true); expect(hasWarningFieldFilter).to.be(true); }); + + it('filter without an index pattern is rendred as a warning, if the dashboard has an index pattern', async function () { + const noIndexPatternFilter = await filterBar.hasFilter('banana', 'warn', true); + expect(noIndexPatternFilter).to.be(true); + }); }); }); } diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 9f3ce667d64b5..ecaa5aa2da97f 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -69,6 +69,16 @@ export default function ({ getService, getPageObjects }) { }); }); + it('renaming a saved query should modify name in breadcrumb', async function () { + const queryName2 = 'Modified Query # 1'; + await PageObjects.discover.loadSavedSearch(queryName1); + await PageObjects.discover.saveSearch(queryName2); + + await retry.try(async function () { + expect(await PageObjects.discover.getCurrentQueryName()).to.be(queryName2); + }); + }); + it('should show the correct hit count', async function () { const expectedHitCount = '14,004'; await retry.try(async function () { diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 06b2e86dd1af9..6e4b820879ed3 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -82,9 +82,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { let objects = await PageObjects.settings.getSavedObjectsInTable(); expect(objects.includes('A Dashboard')).to.be(true); - await PageObjects.common.navigateToActualUrl( - 'kibana', - '/management/kibana/objects/savedDashboards/i-exist' + await PageObjects.common.navigateToUrl( + 'management', + 'kibana/objects/savedDashboards/i-exist', + { + shouldUseHashForSubUrl: false, + } ); await testSubjects.existOrFail('savedObjectEditSave'); @@ -100,9 +103,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(objects.includes('A Dashboard')).to.be(false); expect(objects.includes('Edited Dashboard')).to.be(true); - await PageObjects.common.navigateToActualUrl( - 'kibana', - '/management/kibana/objects/savedDashboards/i-exist' + await PageObjects.common.navigateToUrl( + 'management', + 'kibana/objects/savedDashboards/i-exist', + { + shouldUseHashForSubUrl: false, + } ); expect(await getFieldValue('title')).to.eql('Edited Dashboard'); @@ -110,9 +116,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('allows to delete a saved object', async () => { - await PageObjects.common.navigateToActualUrl( - 'kibana', - '/management/kibana/objects/savedDashboards/i-exist' + await PageObjects.common.navigateToUrl( + 'management', + 'kibana/objects/savedDashboards/i-exist', + { + shouldUseHashForSubUrl: false, + } ); await focusAndClickButton('savedObjectEditDelete'); @@ -124,7 +133,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('preserves the object references when saving', async () => { const testVisualizationUrl = - '/management/kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed'; + 'kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed'; const visualizationRefs = [ { name: 'kibanaSavedObjectMeta.searchSourceJSON.index', @@ -139,7 +148,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const objects = await PageObjects.settings.getSavedObjectsInTable(); expect(objects.includes('A Pie')).to.be(true); - await PageObjects.common.navigateToActualUrl('kibana', testVisualizationUrl); + await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { + shouldUseHashForSubUrl: false, + }); await testSubjects.existOrFail('savedObjectEditSave'); @@ -151,7 +162,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.settings.getSavedObjectsInTable(); - await PageObjects.common.navigateToActualUrl('kibana', testVisualizationUrl); + await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { + shouldUseHashForSubUrl: false, + }); // Parsing to avoid random keys ordering issues in raw string comparison expect(JSON.parse(await getAceEditorFieldValue('references'))).to.eql(visualizationRefs); @@ -162,7 +175,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.settings.getSavedObjectsInTable(); - await PageObjects.common.navigateToActualUrl('kibana', testVisualizationUrl); + await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { + shouldUseHashForSubUrl: false, + }); displayedReferencesValue = await getAceEditorFieldValue('references'); diff --git a/test/functional/apps/visualize/_lab_mode.js b/test/functional/apps/visualize/_lab_mode.js index b356d01cdb63b..27c149b9e0e0a 100644 --- a/test/functional/apps/visualize/_lab_mode.js +++ b/test/functional/apps/visualize/_lab_mode.js @@ -18,6 +18,7 @@ */ import expect from '@kbn/expect'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualizations/common/constants'; export default function ({ getService, getPageObjects }) { const log = getService('log'); @@ -37,7 +38,7 @@ export default function ({ getService, getPageObjects }) { // Navigate to advanced setting and disable lab mode await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs'); + await PageObjects.settings.toggleAdvancedSettingCheckbox(VISUALIZE_ENABLE_LABS_SETTING); // Expect the discover still to list that saved visualization in the open list await PageObjects.header.clickDiscover(); @@ -51,7 +52,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.closeLoadSaveSearchPanel(); await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs'); + await PageObjects.settings.clearAdvancedSettings(VISUALIZE_ENABLE_LABS_SETTING); }); }); } diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index bd427577cd787..42b82486dc13f 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -18,6 +18,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context.d'; +import { UI_SETTINGS } from '../../../../src/plugins/data/common'; // eslint-disable-next-line @typescript-eslint/no-namespace, import/no-default-export export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { @@ -37,7 +38,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid await esArchiver.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', - 'format:bytes:defaultPattern': '0,0.[000]b', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', }); isOss = await PageObjects.common.isOss(); }); diff --git a/test/functional/config.js b/test/functional/config.js index 52806e8ca68ea..c377e64f2fc45 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -83,9 +83,12 @@ export default async function ({ readConfigFile }) { pathname: '/app/dashboards', hash: '/list', }, + management: { + pathname: '/app/management', + }, + /** @obsolete "management" should be instead of "settings" **/ settings: { - pathname: '/app/kibana', - hash: '/management', + pathname: '/app/management', }, timelion: { pathname: '/app/timelion', diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz index a052aad9450f5..ae78761fef0d3 100644 Binary files a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz and b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz differ diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 7810cce5c78bb..fe5694efc35da 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -67,17 +67,17 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo * @param appUrl Kibana URL */ private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { + // Disable the welcome screen. This is relevant for environments + // which don't allow to use the yml setting, e.g. cloud production. + // It is done here so it applies to logins but also to a login re-use. + await browser.setLocalStorageItem('home:welcome:show', 'false'); + let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting const loginPage = currentUrl.includes('/login'); const wantedLoginPage = appUrl.includes('/login') || appUrl.includes('/logout'); - // Disable the welcome screen. This is relevant for environments - // which don't allow to use the yml setting, e.g. cloud production. - // It is done here so it applies to logins but also to a login re-use. - await browser.setLocalStorageItem('home:welcome:show', 'false'); - if (loginPage && !wantedLoginPage) { log.debug('Found login page'); if (config.get('security.disableTestUser')) { @@ -147,13 +147,19 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo shouldLoginIfPrompted = true, useActualUrl = false, insertTimestamp = true, + shouldUseHashForSubUrl = true, } = {} ) { - const appConfig = { + const appConfig: { pathname: string; hash?: string } = { pathname: `${basePath}${config.get(['apps', appName]).pathname}`, - hash: useActualUrl ? subUrl : `/${appName}/${subUrl}`, }; + if (shouldUseHashForSubUrl) { + appConfig.hash = useActualUrl ? subUrl : `/${appName}/${subUrl}`; + } else { + appConfig.pathname += `/${subUrl}`; + } + await this.navigate({ appConfig, ensureCurrentUrl, diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 9cf7a48cb3d88..f5b4eb7ad5de8 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -300,7 +300,9 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getIndexPatternList() { await testSubjects.existOrFail('indexPatternTable', { timeout: 5000 }); - return await find.allByCssSelector('[data-test-subj="indexPatternTable"] .euiTable a'); + return await find.allByCssSelector( + '[data-test-subj="indexPatternTable"] .euiTable .euiTableRow' + ); } async isIndexPatternListEmpty() { diff --git a/test/plugin_functional/plugins/core_provider_plugin/kibana.json b/test/plugin_functional/plugins/core_provider_plugin/kibana.json index 1d5c5824d6b97..8d9b30acab893 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/kibana.json +++ b/test/plugin_functional/plugins/core_provider_plugin/kibana.json @@ -2,7 +2,7 @@ "id": "core_provider_plugin", "version": "0.0.1", "kibanaVersion": "kibana", - "optionalPlugins": ["core_plugin_a", "core_plugin_b", "licensing"], + "optionalPlugins": ["core_plugin_a", "core_plugin_b", "licensing", "globalSearchTest"], "server": false, "ui": true } diff --git a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx index 96297f6d51566..494570b26f561 100644 --- a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter as Router, Switch, Route, Link } from 'react-router-dom'; +import { Router, Switch, Route, Link } from 'react-router-dom'; import { CoreSetup, Plugin } from 'kibana/public'; import { ManagementSetup, ManagementSectionId } from '../../../../../src/plugins/management/public'; @@ -34,19 +34,19 @@ export class ManagementTestPlugin mount(params: any) { params.setBreadcrumbs([{ text: 'Management Test' }]); ReactDOM.render( - +

Hello from management test plugin

- - - Link to /one - - - + Link to basePath + + + Link to /one + +
, params.element diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 64d27103e59e2..6d31889a9cbe4 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -135,7 +135,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide expect(wrapperHeight).to.be.below(windowHeight); }); - it('can navigate from NP apps to legacy apps', async () => { + // Not sure if we need this test or not. If yes, we need to find another legacy app + it.skip('can navigate from NP apps to legacy apps', async () => { await appsMenu.clickLink('Stack Management'); await testSubjects.existOrFail('managementNav'); }); diff --git a/test/plugin_functional/test_suites/management/management_plugin.js b/test/plugin_functional/test_suites/management/management_plugin.js index 87542c97a3f5d..8d879cfa67d7a 100644 --- a/test/plugin_functional/test_suites/management/management_plugin.js +++ b/test/plugin_functional/test_suites/management/management_plugin.js @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }) { describe('management plugin', function describeIndexTests() { before(async () => { - await PageObjects.common.navigateToActualUrl('kibana', 'management'); + await PageObjects.common.navigateToActualUrl('management'); }); it('should be able to navigate to management test app', async () => { @@ -38,11 +38,12 @@ export default function ({ getService, getPageObjects }) { }); it('should redirect when app is disabled', async () => { - await PageObjects.common.navigateToActualUrl( - 'kibana', - 'management/data/test-management-disabled' - ); - await testSubjects.existOrFail('management-landing'); + await PageObjects.common.navigateToUrl('management', 'data/test-management-disabled', { + useActualUrl: true, + shouldUseHashForSubUrl: false, + }); + + await testSubjects.existOrFail('managementHome'); }); }); } diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 778142d95e4b4..60d7f0406f4c9 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -31,4 +31,12 @@ else mkdir -p ../kibana/target/kibana-coverage/functional mv target/kibana-coverage/functional/* ../kibana/target/kibana-coverage/functional/ fi + + echo " -> moving junit output, silently fail in case of no report" + mkdir -p ../kibana/target/junit + mv target/junit/* ../kibana/target/junit/ || echo "copying junit failed" + + echo " -> copying screenshots and html for failures" + cp -r test/functional/screenshots/* ../kibana/test/functional/screenshots/ || echo "copying screenshots failed" + cp -r test/functional/failure_debug ../kibana/test/functional/ || echo "copying html failed" fi diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh new file mode 100644 index 0000000000000..23b83cf946d49 --- /dev/null +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup.sh + +installDir="$PARENT_DIR/install/kibana" +destDir="${installDir}-${CI_WORKER_NUMBER}" +cp -R "$installDir" "$destDir" + +export KIBANA_INSTALL_DIR="$destDir" + +echo " -> Running security solution cypress tests" +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Security solution Cypress Tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/security_solution_cypress/config.ts + +echo "" +echo "" diff --git a/test/scripts/jenkins_siem_cypress.sh b/test/scripts/jenkins_siem_cypress.sh deleted file mode 100644 index c7157e97b36cc..0000000000000 --- a/test/scripts/jenkins_siem_cypress.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup.sh - -installDir="$PARENT_DIR/install/kibana" -destDir="${installDir}-${CI_WORKER_NUMBER}" -cp -R "$installDir" "$destDir" - -export KIBANA_INSTALL_DIR="$destDir" - -echo " -> Running SIEM cypress tests" -cd "$XPACK_DIR" - -checks-reporter-with-killswitch "SIEM Cypress Tests" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --config test/siem_cypress/config.ts - -echo "" -echo "" diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 50a92a41e3932..067ed213c49f5 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -17,7 +17,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> Running SIEM cyclic dependency test" cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/siem/scripts/check_circular_deps + checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps echo "" echo "" diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index a6e600630364e..648605135b359 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -32,4 +32,12 @@ else mkdir -p ../../kibana/target/kibana-coverage/functional mv ../target/kibana-coverage/functional/* ../../kibana/target/kibana-coverage/functional/ fi + + echo " -> moving junit output, silently fail in case of no report" + mkdir -p ../../kibana/target/junit + mv ../target/junit/* ../../kibana/target/junit/ || echo "copying junit failed" + + echo " -> copying screenshots and html for failures" + cp -r test/functional/screenshots/* ../../kibana/x-pack/test/functional/screenshots/ || echo "copying screenshots failed" + cp -r test/functional/failure_debug ../../kibana/x-pack/test/functional/ || echo "copying html failed" fi \ No newline at end of file diff --git a/test/scripts/jenkins_xpack_page_load_metrics.sh b/test/scripts/jenkins_xpack_page_load_metrics.sh new file mode 100644 index 0000000000000..679f0b8d2ddc5 --- /dev/null +++ b/test/scripts/jenkins_xpack_page_load_metrics.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_xpack.sh + +checks-reporter-with-killswitch "Capture Kibana page load metrics" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$installDir" \ + --config test/page_load_metrics/config.ts; diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 6ef8f6fc64cd5..66b16566418b5 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -96,9 +96,9 @@ def collectVcsInfo(title) { ) } -def bootMergeAndIngest(buildNum, buildUrl, title) { +def generateReports(title) { kibanaPipeline.bash(""" - source src/dev/ci_setup/setup_env.sh + source src/dev/ci_setup/setup_env.sh true # bootstrap from x-pack folder cd x-pack yarn kbn bootstrap --prefer-offline @@ -108,6 +108,28 @@ def bootMergeAndIngest(buildNum, buildUrl, title) { . src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh . src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh . src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh + # zip combined reports + tar -czf kibana-coverage.tar.gz target/kibana-coverage/**/* + """, title) +} + +def uploadCombinedReports() { + kibanaPipeline.bash(""" + ls -laR target/kibana-coverage/ + """, "### List Combined Reports" + ) + + kibanaPipeline.uploadGcsArtifact( + "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/coverage/combined", + 'kibana-coverage.tar.gz' + ) +} + +def ingestData(buildNum, buildUrl, title) { + kibanaPipeline.bash(""" + source src/dev/ci_setup/setup_env.sh + yarn kbn bootstrap --prefer-offline + # Using existing target/kibana-coverage folder . src/dev/code_coverage/shell_scripts/ingest_coverage.sh ${buildNum} ${buildUrl} """, title) } @@ -117,7 +139,7 @@ def ingestWithVault(buildNum, buildUrl, title) { withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - bootMergeAndIngest(buildNum, buildUrl, title) + ingestData(buildNum, buildUrl, title) } } } diff --git a/webpackShims/numeral.js b/webpackShims/numeral.js deleted file mode 100644 index d9551e05aa6d6..0000000000000 --- a/webpackShims/numeral.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = require('@elastic/numeral'); diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 6bac5e181861d..68262c4bf734b 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -3,13 +3,16 @@ /target /test/functional/failure_debug /test/functional/screenshots +/test/page_load_metrics/screenshots /test/functional/apps/reporting/reports/session /test/reporting/configs/failure_debug/ /legacy/plugins/reporting/.chromium/ /legacy/plugins/reporting/.phantom/ +/plugins/reporting/.chromium/ +/plugins/reporting/.phantom/ /.aws-config.json /.env /.kibana-plugin-helpers.dev.* !/legacy/plugins/infra/**/target .cache -!/legacy/plugins/siem/**/target +!/legacy/plugins/security_solution/**/target diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index a479b08ef9069..85b40d33c4089 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,8 +2,7 @@ "prefix": "xpack", "paths": { "xpack.actions": "plugins/actions", - "xpack.advancedUiActions": "plugins/advanced_ui_actions", - "xpack.uiActionsEnhanced": "examples/ui_actions_enhanced_examples", + "xpack.uiActionsEnhanced": ["plugins/ui_actions_enhanced", "examples/ui_actions_enhanced_examples"], "xpack.alerts": "plugins/alerts", "xpack.alertingBuiltins": "plugins/alerting_builtins", "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], @@ -18,6 +17,7 @@ "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", + "xpack.globalSearch": ["plugins/global_search"], "xpack.graph": ["plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", @@ -35,12 +35,12 @@ "xpack.monitoring": ["plugins/monitoring", "legacy/plugins/monitoring"], "xpack.remoteClusters": "plugins/remote_clusters", "xpack.painlessLab": "plugins/painless_lab", - "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], + "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["legacy/plugins/rollup", "plugins/rollup"], "xpack.searchProfiler": "plugins/searchprofiler", - "xpack.security": ["legacy/plugins/security", "plugins/security"], + "xpack.security": "plugins/security", "xpack.server": "legacy/server", - "xpack.siem": "plugins/siem", + "xpack.securitySolution": "plugins/security_solution", "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", @@ -48,7 +48,8 @@ "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": ["plugins/uptime"], - "xpack.watcher": "plugins/watcher" + "xpack.watcher": "plugins/watcher", + "xpack.observability": "plugins/observability" }, "translations": [ "plugins/translations/translations/zh-CN.json", diff --git a/x-pack/.kibana-plugin-helpers.json b/x-pack/.kibana-plugin-helpers.json index ed0b38841c1a8..ca0cae6dd3ca3 100644 --- a/x-pack/.kibana-plugin-helpers.json +++ b/x-pack/.kibana-plugin-helpers.json @@ -13,8 +13,8 @@ "index.js", ".i18nrc.json", "plugins/**/*", - "legacy/plugins/reporting/.phantom/*", - "legacy/plugins/reporting/.chromium/*", + "plugins/reporting/.phantom/*", + "plugins/reporting/.chromium/*", "legacy/common/**/*", "legacy/plugins/**/*", "legacy/server/**/*", diff --git a/x-pack/README.md b/x-pack/README.md index 744d97ca02c75..03d2e3287c0f0 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -25,8 +25,8 @@ Examples: - Run the jest test case whose description matches 'filtering should skip values of null': `cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js` - Run the x-pack api integration test case whose description matches the given string: - `node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` - `node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'` + `node scripts/functional_tests_server --config x-pack/test/api_integration/config.ts` + `node scripts/functional_test_runner --config x-pack/test/api_integration/config.ts --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'` In addition to to providing a regular expression argument, specific tests can also be run by appeding `.only` to an `it` or `describe` function block. E.g. `describe(` to `describe.only(`. @@ -63,7 +63,7 @@ yarn test:mocha For more info, see [the Elastic functional test development guide](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html). -The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.js)), and *SAML API integration tests* ([specified by this config](test/saml_api_integration/config.js)). +The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/saml_api_integration/config.ts)). The script runs all sets of tests sequentially like so: * builds Elasticsearch and X-Pack diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md index 501136ab0a965..72e41afc80c95 100644 --- a/x-pack/build_chromium/README.md +++ b/x-pack/build_chromium/README.md @@ -110,7 +110,7 @@ To run the build, replace the sha in the following commands with the sha that yo After the build completes, there will be a .zip file and a .md5 file in `~/chromium/chromium/src/out/headless`. These are named like so: `chromium-{first_7_of_SHA}-{platform}`, for example: `chromium-4747cc2-linux`. -The zip files need to be deployed to s3. For testing, I drop them into `headless-shell-dev`, but for production, they need to be in `headless-shell`. And the `x-pack/legacy/plugins/reporting/server/browsers/chromium/paths.js` file needs to be upated to have the correct `archiveChecksum`, `archiveFilename`, `binaryChecksum` and `baseUrl`. Below is a list of what the archive's are: +The zip files need to be deployed to s3. For testing, I drop them into `headless-shell-dev`, but for production, they need to be in `headless-shell`. And the `x-pack/plugins/reporting/server/browsers/chromium/paths.ts` file needs to be upated to have the correct `archiveChecksum`, `archiveFilename`, `binaryChecksum` and `baseUrl`. Below is a list of what the archive's are: - `archiveChecksum`: The contents of the `.md5` file, which is the `md5` checksum of the zip file. - `binaryChecksum`: The `md5` checksum of the `headless_shell` binary itself. diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a222e11d28f4a..74553bbde0cd6 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -24,7 +24,8 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, - '\\.ace\\.worker.js$': `${kibanaDirectory}/src/dev/jest/mocks/ace_worker_module_mock.js`, + '\\.ace\\.worker.js$': `${kibanaDirectory}/src/dev/jest/mocks/worker_module_mock.js`, + '\\.editor\\.worker.js$': `${kibanaDirectory}/src/dev/jest/mocks/worker_module_mock.js`, '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, @@ -42,6 +43,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '!**/scripts/**', '!**/mocks/**', '!**/plugins/apm/e2e/**', + '!**/plugins/siem/cypress/**', ], coveragePathIgnorePatterns: ['.*\\.d\\.ts'], coverageDirectory: `${kibanaDirectory}/target/kibana-coverage/jest`, diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index e220cdd5cd297..a1cd895bb3cd6 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -5,6 +5,6 @@ "configPath": ["ui_actions_enhanced_examples"], "server": false, "ui": true, - "requiredPlugins": ["advancedUiActions", "data"], + "requiredPlugins": ["uiActionsEnhanced", "data", "discover"], "optionalPlugins": [] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx index 847035403da02..bfe853241ae1d 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { RangeSelectTriggerContext, ValueClickTriggerContext, diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx index 0237e128c5a2f..da9b0e921fb1c 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx @@ -31,9 +31,7 @@ export const DiscoverDrilldownConfig: React.FC = ( onIndexPatternSelect, customIndexPattern, onCustomIndexPatternToggle, - carryFiltersAndQuery, onCarryFiltersAndQueryToggle, - carryTimeRange, onCarryTimeRangeToggle, }) => { return ( @@ -82,9 +80,10 @@ export const DiscoverDrilldownConfig: React.FC = ( {!!onCarryFiltersAndQueryToggle && ( @@ -92,9 +91,10 @@ export const DiscoverDrilldownConfig: React.FC = ( {!!onCarryTimeRangeToggle && ( diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx index fef01c9640f0d..ba88f49861ffe 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx @@ -11,7 +11,7 @@ import { StartServicesGetter } from '../../../../../src/plugins/kibana_utils/pub import { ActionContext, Config, CollectConfigProps } from './types'; import { CollectConfigContainer } from './collect_config_container'; import { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { txtGoToDiscover } from './i18n'; const isOutputWithIndexPatterns = ( @@ -22,7 +22,7 @@ const isOutputWithIndexPatterns = ( }; export interface Params { - start: StartServicesGetter>; + start: StartServicesGetter>; } export class DashboardToDiscoverDrilldown implements Drilldown { @@ -54,6 +54,10 @@ export class DashboardToDiscoverDrilldown implements Drilldown => { + const { urlGenerator } = this.params.start().plugins.discover; + + if (!urlGenerator) throw new Error('Discover URL generator not available.'); + let indexPatternId = !!config.customIndexPattern && !!config.indexPatternId ? config.indexPatternId : ''; @@ -64,8 +68,9 @@ export class DashboardToDiscoverDrilldown implements Drilldown => { diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 20267a8b7292b..4810fb2d6ad8d 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { RangeSelectTriggerContext, ValueClickTriggerContext, diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts index 0d4f274caf57f..8034c378cc64f 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts @@ -9,27 +9,30 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/pl import { AdvancedUiActionsSetup, AdvancedUiActionsStart, -} from '../../../../x-pack/plugins/advanced_ui_actions/public'; +} from '../../../../x-pack/plugins/ui_actions_enhanced/public'; import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown'; import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown'; import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; +import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; export interface SetupDependencies { data: DataPublicPluginSetup; - advancedUiActions: AdvancedUiActionsSetup; + discover: DiscoverSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface StartDependencies { data: DataPublicPluginStart; - advancedUiActions: AdvancedUiActionsStart; + discover: DiscoverStart; + uiActionsEnhanced: AdvancedUiActionsStart; } export class UiActionsEnhancedExamplesPlugin implements Plugin { public setup( core: CoreSetup, - { advancedUiActions: uiActions }: SetupDependencies + { uiActionsEnhanced: uiActions }: SetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); diff --git a/x-pack/index.js b/x-pack/index.js index 9cf63854d4093..8096774d7a491 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -6,7 +6,6 @@ import { xpackMain } from './legacy/plugins/xpack_main'; import { monitoring } from './legacy/plugins/monitoring'; -import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { beats } from './legacy/plugins/beats_management'; @@ -18,7 +17,6 @@ module.exports = function (kibana) { return [ xpackMain(kibana), monitoring(kibana), - reporting(kibana), spaces(kibana), security(kibana), dashboardMode(kibana), diff --git a/x-pack/legacy/plugins/beats_management/readme.md b/x-pack/legacy/plugins/beats_management/readme.md index 301caad683dd5..3414f09deed46 100644 --- a/x-pack/legacy/plugins/beats_management/readme.md +++ b/x-pack/legacy/plugins/beats_management/readme.md @@ -15,7 +15,7 @@ In one shell, from **~/kibana/x-pack**: `node scripts/functional_tests-server.js` In another shell, from **~kibana/x-pack**: -`node ../scripts/functional_test_runner.js --config test/api_integration/config.js`. +`node ../scripts/functional_test_runner.js --config test/api_integration/config.ts`. ### Manual e2e testing diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index bb1f68e1c03b3..80599f38d982a 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -8,6 +8,7 @@ import { Lifecycle, ResponseToolkit } from 'hapi'; import * as t from 'io-ts'; +import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; import { LicenseType } from '../../../../common/constants/security'; export const internalAuthData = Symbol('internalAuthData'); @@ -39,6 +40,11 @@ export interface BackendFrameworkAdapter { } export interface KibanaLegacyServer { + newPlatform: { + setup: { + plugins: { security: SecurityPluginSetup }; + }; + }; plugins: { xpack_main: { status: { @@ -53,9 +59,6 @@ export interface KibanaLegacyServer { }; }; }; - security: { - getUser: (request: KibanaServerRequest) => any; - }; elasticsearch: { status: { on: (status: 'green' | 'yellow' | 'red', callback: () => void) => void; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts index 589f34ac74601..1bf9bbb22b352 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -8,6 +8,7 @@ import { ResponseToolkit } from 'hapi'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { get } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; +import { KibanaRequest, LegacyRequest } from '../../../../../../../../src/core/server'; // @ts-ignore import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; import { @@ -128,13 +129,10 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { } private async getUser(request: KibanaServerRequest): Promise { - let user; - try { - user = await this.server.plugins.security.getUser(request); - } catch (e) { - return null; - } - if (user === null) { + const user = this.server.newPlatform.setup.plugins.security?.authc.getCurrentUser( + KibanaRequest.from((request as unknown) as LegacyRequest) + ); + if (!user) { return null; } const assertKibanaUser = RuntimeKibanaUser.decode(user); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/index.ts deleted file mode 100644 index 993b8f6cdc9ab..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/index.ts +++ /dev/null @@ -1,66 +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 { HeadlessChromiumDriver } from '../../../server/browsers'; -import { LevelLogger } from '../../../server/lib'; -import { Layout } from './layout'; - -export { createLayout } from './create_layout'; -export { Layout } from './layout'; -export { PreserveLayout } from './preserve_layout'; -export { PrintLayout } from './print_layout'; - -export const LayoutTypes = { - PRESERVE_LAYOUT: 'preserve_layout', - PRINT: 'print', -}; - -export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ - screenshot: '[data-shared-items-container]', - renderComplete: '[data-shared-item]', - itemsCountAttribute: 'data-shared-items-count', - timefilterDurationAttribute: 'data-shared-timefilter-duration', -}); - -export interface PageSizeParams { - pageMarginTop: number; - pageMarginBottom: number; - pageMarginWidth: number; - tableBorderWidth: number; - headingHeight: number; - subheadingHeight: number; -} - -export interface LayoutSelectorDictionary { - screenshot: string; - renderComplete: string; - itemsCountAttribute: string; - timefilterDurationAttribute: string; -} - -export interface PdfImageSize { - width: number; - height?: number; -} - -export interface Size { - width: number; - height: number; -} - -export interface LayoutParams { - id: string; - dimensions: Size; -} - -interface LayoutSelectors { - // Fields that are not part of Layout: the instances - // independently implement these fields on their own - selectors: LayoutSelectorDictionary; - positionElements?: (browser: HeadlessChromiumDriver, logger: LevelLogger) => Promise; -} - -export type LayoutInstance = Layout & LayoutSelectors & Size; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv/index.ts deleted file mode 100644 index cdb4c36dba3df..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/index.ts +++ /dev/null @@ -1,45 +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 { - CSV_JOB_TYPE as jobType, - LICENSE_TYPE_BASIC, - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_TRIAL, -} from '../../common/constants'; -import { - ESQueueCreateJobFn, - ESQueueWorkerExecuteFn, - ExportTypeDefinition, -} from '../../server/types'; -import { metadata } from './metadata'; -import { createJobFactory } from './server/create_job'; -import { executeJobFactory } from './server/execute_job'; -import { JobDocPayloadDiscoverCsv, JobParamsDiscoverCsv } from './types'; - -export const getExportType = (): ExportTypeDefinition< - JobParamsDiscoverCsv, - ESQueueCreateJobFn, - JobDocPayloadDiscoverCsv, - ESQueueWorkerExecuteFn -> => ({ - ...metadata, - jobType, - jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, - validLicenses: [ - LICENSE_TYPE_TRIAL, - LICENSE_TYPE_BASIC, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - ], -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts deleted file mode 100644 index c76b4afe727da..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts +++ /dev/null @@ -1,40 +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 { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { ReportingCore } from '../../../server'; -import { cryptoFactory } from '../../../server/lib'; -import { CreateJobFactory, ESQueueCreateJobFn } from '../../../server/types'; -import { JobParamsDiscoverCsv } from '../types'; - -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - const setupDeps = reporting.getPluginSetupDeps(); - - return async function createJob( - jobParams: JobParamsDiscoverCsv, - context: RequestHandlerContext, - request: KibanaRequest - ) { - const serializedEncryptedHeaders = await crypto.encrypt(request.headers); - - const savedObjectsClient = context.core.savedObjects.client; - const indexPatternSavedObject = await savedObjectsClient.get( - 'index-pattern', - jobParams.indexPatternId! - ); - - return { - headers: serializedEncryptedHeaders, - indexPatternSavedObject, - basePath: setupDeps.basePath(request), - ...jobParams, - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts deleted file mode 100644 index a6b2b0d0561d0..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ /dev/null @@ -1,153 +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 { i18n } from '@kbn/i18n'; -import Hapi from 'hapi'; -import { IUiSettingsClient, KibanaRequest } from '../../../../../../../src/core/server'; -import { CSV_BOM_CHARS, CSV_JOB_TYPE } from '../../../common/constants'; -import { ReportingCore } from '../../../server'; -import { cryptoFactory, LevelLogger } from '../../../server/lib'; -import { getFieldFormats } from '../../../server/services'; -import { ESQueueWorkerExecuteFn, ExecuteJobFactory } from '../../../server/types'; -import { JobDocPayloadDiscoverCsv } from '../types'; -import { fieldFormatMapFactory } from './lib/field_format_map'; -import { createGenerateCsv } from './lib/generate_csv'; - -export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); - const serverBasePath = config.kbnConfig.get('server', 'basePath'); - - return async function executeJob( - jobId: string, - job: JobDocPayloadDiscoverCsv, - cancellationToken: any - ) { - const elasticsearch = await reporting.getElasticsearchService(); - const jobLogger = logger.clone([jobId]); - - const { - searchRequest, - fields, - indexPatternSavedObject, - metaFields, - conflictedTypesFields, - headers, - basePath, - } = job; - - const decryptHeaders = async () => { - try { - if (typeof headers !== 'string') { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage', - { - defaultMessage: 'Job headers are missing', - } - ) - ); - } - return await crypto.decrypt(headers); - } catch (err) { - logger.error(err); - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage', - { - defaultMessage: 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', - values: { encryptionKey: 'xpack.reporting.encryptionKey', err: err.toString() }, - } - ) - ); // prettier-ignore - } - }; - - const fakeRequest = KibanaRequest.from({ - headers: await decryptHeaders(), - // This is used by the spaces SavedObjectClientWrapper to determine the existing space. - // We use the basePath from the saved job, which we'll have post spaces being implemented; - // or we use the server base path, which uses the default space - getBasePath: () => basePath || serverBasePath, - path: '/', - route: { settings: {} }, - url: { href: '/' }, - raw: { req: { url: '/' } }, - } as Hapi.Request); - - const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(fakeRequest); - const callEndpoint = (endpoint: string, clientParams = {}, options = {}) => - callAsCurrentUser(endpoint, clientParams, options); - - const savedObjectsClient = await reporting.getSavedObjectsClient(fakeRequest); - const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - - const getFormatsMap = async (client: IUiSettingsClient) => { - const fieldFormats = await getFieldFormats().fieldFormatServiceFactory(client); - return fieldFormatMapFactory(indexPatternSavedObject, fieldFormats); - }; - const getUiSettings = async (client: IUiSettingsClient) => { - const [separator, quoteValues, timezone] = await Promise.all([ - client.get('csv:separator'), - client.get('csv:quoteValues'), - client.get('dateFormat:tz'), - ]); - - if (timezone === 'Browser') { - logger.warn( - i18n.translate('xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting', { - defaultMessage: 'Kibana Advanced Setting "{dateFormatTimezone}" is set to "Browser". Dates will be formatted as UTC to avoid ambiguity.', - values: { dateFormatTimezone: 'dateFormat:tz' } - }) - ); // prettier-ignore - } - - return { - separator, - quoteValues, - timezone, - }; - }; - - const [formatsMap, uiSettings] = await Promise.all([ - getFormatsMap(uiSettingsClient), - getUiSettings(uiSettingsClient), - ]); - - const generateCsv = createGenerateCsv(jobLogger); - const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; - - const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv({ - searchRequest, - fields, - metaFields, - conflictedTypesFields, - callEndpoint, - cancellationToken, - formatsMap, - settings: { - ...uiSettings, - checkForFormulas: config.get('csv', 'checkForFormulas'), - maxSizeBytes: config.get('csv', 'maxSizeBytes'), - scroll: config.get('csv', 'scroll'), - escapeFormulaValues: config.get('csv', 'escapeFormulaValues'), - }, - }); - - // @TODO: Consolidate these one-off warnings into the warnings array (max-size reached and csv contains formulas) - return { - content_type: 'text/csv', - content: bom + content, - max_size_reached: maxSizeReached, - size, - csv_contains_formulas: csvContainsFormulas, - warnings, - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts deleted file mode 100644 index a8fdd8d1a5bbc..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts +++ /dev/null @@ -1,105 +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 { i18n } from '@kbn/i18n'; -import { LevelLogger } from '../../../../server/lib'; -import { GenerateCsvParams, SavedSearchGeneratorResult } from '../../types'; -import { createFlattenHit } from './flatten_hit'; -import { createFormatCsvValues } from './format_csv_values'; -import { createEscapeValue } from './escape_value'; -import { createHitIterator } from './hit_iterator'; -import { MaxSizeStringBuilder } from './max_size_string_builder'; -import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; - -export function createGenerateCsv(logger: LevelLogger) { - const hitIterator = createHitIterator(logger); - - return async function generateCsv({ - searchRequest, - fields, - formatsMap, - metaFields, - conflictedTypesFields, - callEndpoint, - cancellationToken, - settings, - }: GenerateCsvParams): Promise { - const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); - const builder = new MaxSizeStringBuilder(settings.maxSizeBytes); - const header = `${fields.map(escapeValue).join(settings.separator)}\n`; - const warnings: string[] = []; - - if (!builder.tryAppend(header)) { - return { - size: 0, - content: '', - maxSizeReached: true, - warnings: [], - }; - } - - const iterator = hitIterator(settings.scroll, callEndpoint, searchRequest, cancellationToken); - let maxSizeReached = false; - let csvContainsFormulas = false; - - const flattenHit = createFlattenHit(fields, metaFields, conflictedTypesFields); - const formatCsvValues = createFormatCsvValues( - escapeValue, - settings.separator, - fields, - formatsMap - ); - try { - while (true) { - const { done, value: hit } = await iterator.next(); - - if (!hit) { - break; - } - - if (done) { - break; - } - - const flattened = flattenHit(hit); - const rows = formatCsvValues(flattened); - const rowsHaveFormulas = - settings.checkForFormulas && checkIfRowsHaveFormulas(flattened, fields); - - if (rowsHaveFormulas) { - csvContainsFormulas = true; - } - - if (!builder.tryAppend(rows + '\n')) { - logger.warn('max Size Reached'); - maxSizeReached = true; - cancellationToken.cancel(); - break; - } - } - } finally { - await iterator.return(); - } - const size = builder.getSizeInBytes(); - logger.debug(`finished generating, total size in bytes: ${size}`); - - if (csvContainsFormulas && settings.escapeFormulaValues) { - warnings.push( - i18n.translate('xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues', { - defaultMessage: 'CSV may contain formulas whose values have been escaped', - }) - ); - } - - return { - content: builder.getString(), - csvContainsFormulas: csvContainsFormulas && !settings.escapeFormulaValues, - maxSizeReached, - size, - warnings, - }; - }; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts deleted file mode 100644 index 0f6223d8553de..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts +++ /dev/null @@ -1,116 +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 { CancellationToken } from '../../../../../plugins/reporting/common'; -import { JobDocPayload, JobParamPostPayload, ScrollConfig } from '../../server/types'; - -export type RawValue = string | object | null | undefined; - -interface DocValueField { - field: string; - format: string; -} - -interface SortOptions { - order: string; - unmapped_type: string; -} - -export interface JobParamPostPayloadDiscoverCsv extends JobParamPostPayload { - state?: { - query: any; - sort: Array>; - docvalue_fields: DocValueField[]; - }; -} - -export interface JobParamsDiscoverCsv { - indexPatternId?: string; - post?: JobParamPostPayloadDiscoverCsv; -} - -export interface JobDocPayloadDiscoverCsv extends JobDocPayload { - basePath: string; - searchRequest: any; - fields: any; - indexPatternSavedObject: any; - metaFields: any; - conflictedTypesFields: any; -} - -export interface SearchRequest { - index: string; - body: - | { - _source: { - excludes: string[]; - includes: string[]; - }; - docvalue_fields: string[]; - query: - | { - bool: { - filter: any[]; - must_not: any[]; - should: any[]; - must: any[]; - }; - } - | any; - script_fields: any; - sort: Array<{ - [key: string]: { - order: string; - }; - }>; - stored_fields: string[]; - } - | any; -} - -type EndpointCaller = (method: string, params: any) => Promise; - -type FormatsMap = Map< - string, - { - id: string; - params: { - pattern: string; - }; - } ->; - -export interface SavedSearchGeneratorResult { - content: string; - size: number; - maxSizeReached: boolean; - csvContainsFormulas?: boolean; - warnings: string[]; -} - -export interface CsvResultFromSearch { - type: string; - result: SavedSearchGeneratorResult; -} - -export interface GenerateCsvParams { - searchRequest: SearchRequest; - callEndpoint: EndpointCaller; - fields: string[]; - formatsMap: FormatsMap; - metaFields: string[]; - conflictedTypesFields: string[]; - cancellationToken: CancellationToken; - settings: { - separator: string; - quoteValues: boolean; - timezone: string | null; - maxSizeBytes: number; - scroll: ScrollConfig; - checkForFormulas?: boolean; - escapeFormulaValues: boolean; - }; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts deleted file mode 100644 index 570b91600cbe0..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts +++ /dev/null @@ -1,48 +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 { - CSV_FROM_SAVEDOBJECT_JOB_TYPE, - LICENSE_TYPE_BASIC, - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_TRIAL, -} from '../../common/constants'; -import { ExportTypeDefinition } from '../../server/types'; -import { metadata } from './metadata'; -import { createJobFactory, ImmediateCreateJobFn } from './server/create_job'; -import { executeJobFactory, ImmediateExecuteFn } from './server/execute_job'; -import { JobParamsPanelCsv } from './types'; - -/* - * These functions are exported to share with the API route handler that - * generates csv from saved object immediately on request. - */ -export { createJobFactory } from './server/create_job'; -export { executeJobFactory } from './server/execute_job'; - -export const getExportType = (): ExportTypeDefinition< - JobParamsPanelCsv, - ImmediateCreateJobFn, - JobParamsPanelCsv, - ImmediateExecuteFn -> => ({ - ...metadata, - jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, - jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, - validLicenses: [ - LICENSE_TYPE_TRIAL, - LICENSE_TYPE_BASIC, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - ], -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts deleted file mode 100644 index d23f60d9c2480..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ /dev/null @@ -1,106 +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 { notFound, notImplemented } from 'boom'; -import { get } from 'lodash'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { ReportingCore } from '../../../../server'; -import { cryptoFactory, LevelLogger } from '../../../../server/lib'; -import { CreateJobFactory, TimeRangeParams } from '../../../../server/types'; -import { - JobDocPayloadPanelCsv, - JobParamsPanelCsv, - SavedObject, - SavedObjectServiceError, - SavedSearchObjectAttributesJSON, - SearchPanel, - VisObjectAttributesJSON, -} from '../../types'; -import { createJobSearch } from './create_job_search'; - -export type ImmediateCreateJobFn = ( - jobParams: JobParamsType, - headers: KibanaRequest['headers'], - context: RequestHandlerContext, - req: KibanaRequest -) => Promise<{ - type: string | null; - title: string; - jobParams: JobParamsType; -}>; - -interface VisData { - title: string; - visType: string; - panel: SearchPanel; -} - -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); - - return async function createJob( - jobParams: JobParamsPanelCsv, - headers: KibanaRequest['headers'], - context: RequestHandlerContext, - req: KibanaRequest - ): Promise { - const { savedObjectType, savedObjectId } = jobParams; - const serializedEncryptedHeaders = await crypto.encrypt(headers); - - const { panel, title, visType }: VisData = await Promise.resolve() - .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) - .then(async (savedObject: SavedObject) => { - const { attributes, references } = savedObject; - const { - kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, - } = attributes as SavedSearchObjectAttributesJSON; - const { timerange } = req.body as { timerange: TimeRangeParams }; - - if (!kibanaSavedObjectMetaJSON) { - throw new Error('Could not parse saved object data!'); - } - - const kibanaSavedObjectMeta = { - ...kibanaSavedObjectMetaJSON, - searchSource: JSON.parse(kibanaSavedObjectMetaJSON.searchSourceJSON), - }; - - const { visState: visStateJSON } = attributes as VisObjectAttributesJSON; - if (visStateJSON) { - throw notImplemented('Visualization types are not yet implemented'); - } - - // saved search type - return await createJobSearch(timerange, attributes, references, kibanaSavedObjectMeta); - }) - .catch((err: Error) => { - const boomErr = (err as unknown) as { isBoom: boolean }; - if (boomErr.isBoom) { - throw err; - } - const errPayload: SavedObjectServiceError = get(err, 'output.payload', { statusCode: 0 }); - if (errPayload.statusCode === 404) { - throw notFound(errPayload.message); - } - if (err.stack) { - logger.error(err.stack); - } - throw new Error(`Unable to create a job from saved object data! Error: ${err}`); - }); - - return { - headers: serializedEncryptedHeaders, - jobParams: { ...jobParams, panel, visType }, - type: null, - title, - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts deleted file mode 100644 index 4ef7b8514b363..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ /dev/null @@ -1,131 +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 { i18n } from '@kbn/i18n'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { ReportingCore } from '../../../server'; -import { cryptoFactory, LevelLogger } from '../../../server/lib'; -import { ExecuteJobFactory, JobDocOutput, JobDocPayload } from '../../../server/types'; -import { CsvResultFromSearch } from '../../csv/types'; -import { FakeRequest, JobDocPayloadPanelCsv, JobParamsPanelCsv, SearchPanel } from '../types'; -import { createGenerateCsv } from './lib'; - -/* - * ImmediateExecuteFn receives the job doc payload because the payload was - * generated in the CreateFn - */ -export type ImmediateExecuteFn = ( - jobId: null, - job: JobDocPayload, - context: RequestHandlerContext, - req: KibanaRequest -) => Promise; - -export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); - const generateCsv = createGenerateCsv(reporting, parentLogger); - - return async function executeJob( - jobId: string | null, - job: JobDocPayloadPanelCsv, - context, - req - ): Promise { - // There will not be a jobID for "immediate" generation. - // jobID is only for "queued" jobs - // Use the jobID as a logging tag or "immediate" - const jobLogger = logger.clone([jobId === null ? 'immediate' : jobId]); - - const { jobParams } = job; - const { isImmediate, panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; - - if (!panel) { - i18n.translate( - 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToAccessPanel', - { defaultMessage: 'Failed to access panel metadata for job execution' } - ); - } - - jobLogger.debug(`Execute job generating [${visType}] csv`); - - let requestObject: KibanaRequest | FakeRequest; - - if (isImmediate && req) { - jobLogger.info(`Executing job from Immediate API using request context`); - requestObject = req; - } else { - jobLogger.info(`Executing job async using encrypted headers`); - let decryptedHeaders: Record; - const serializedEncryptedHeaders = job.headers; - try { - if (typeof serializedEncryptedHeaders !== 'string') { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage', - { - defaultMessage: 'Job headers are missing', - } - ) - ); - } - decryptedHeaders = (await crypto.decrypt(serializedEncryptedHeaders)) as Record< - string, - unknown - >; - } catch (err) { - jobLogger.error(err); - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage', - { - defaultMessage: - 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', - values: { encryptionKey: 'xpack.reporting.encryptionKey', err }, - } - ) - ); - } - - requestObject = { headers: decryptedHeaders }; - } - - let content: string; - let maxSizeReached = false; - let size = 0; - try { - const generateResults: CsvResultFromSearch = await generateCsv( - context, - requestObject, - visType as string, - panel, - jobParams - ); - - ({ - result: { content, maxSizeReached, size }, - } = generateResults); - } catch (err) { - jobLogger.error(`Generate CSV Error! ${err}`); - throw err; - } - - if (maxSizeReached) { - jobLogger.warn(`Max size reached: CSV output truncated to ${size} bytes`); - } - - return { - content_type: CONTENT_TYPE_CSV, - content, - max_size_reached: maxSizeReached, - size, - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv.ts deleted file mode 100644 index 2397da9337890..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv.ts +++ /dev/null @@ -1,41 +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 { badRequest } from 'boom'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { LevelLogger } from '../../../../server/lib'; -import { ReportingCore } from '../../../../server'; -import { FakeRequest, JobParamsPanelCsv, SearchPanel, VisPanel } from '../../types'; -import { generateCsvSearch } from './generate_csv_search'; - -export function createGenerateCsv(reporting: ReportingCore, logger: LevelLogger) { - return async function generateCsv( - context: RequestHandlerContext, - request: KibanaRequest | FakeRequest, - visType: string, - panel: VisPanel | SearchPanel, - jobParams: JobParamsPanelCsv - ) { - // This should support any vis type that is able to fetch - // and model data on the server-side - - // This structure will not be needed when the vis data just consists of an - // expression that we could run through the interpreter to get csv - switch (visType) { - case 'search': - return await generateCsvSearch( - reporting, - context, - request as KibanaRequest, - panel as SearchPanel, - jobParams, - logger - ); - default: - throw badRequest(`Unsupported or unrecognized saved object type: ${visType}`); - } - }; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts deleted file mode 100644 index f838268078503..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts +++ /dev/null @@ -1,163 +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 { JobDocPayload, JobParamPostPayload, TimeRangeParams } from '../../server/types'; - -export interface FakeRequest { - headers: Record; -} - -export interface JobParamsPostPayloadPanelCsv extends JobParamPostPayload { - state?: any; -} - -export interface JobParamsPanelCsv { - savedObjectType: string; - savedObjectId: string; - isImmediate: boolean; - panel?: SearchPanel; - post?: JobParamsPostPayloadPanelCsv; - visType?: string; -} - -export interface JobDocPayloadPanelCsv extends JobDocPayload { - jobParams: JobParamsPanelCsv; -} - -export interface SavedObjectServiceError { - statusCode: number; - error?: string; - message?: string; -} - -export interface SavedObjectMetaJSON { - searchSourceJSON: string; -} - -export interface SavedObjectMeta { - searchSource: SearchSource; -} - -export interface SavedSearchObjectAttributesJSON { - title: string; - sort: any[]; - columns: string[]; - kibanaSavedObjectMeta: SavedObjectMetaJSON; - uiState: any; -} - -export interface SavedSearchObjectAttributes { - title: string; - sort: any[]; - columns?: string[]; - kibanaSavedObjectMeta: SavedObjectMeta; - uiState: any; -} - -export interface VisObjectAttributesJSON { - title: string; - visState: string; // JSON string - type: string; - params: any; - uiStateJSON: string; // also JSON string - aggs: any[]; - sort: any[]; - kibanaSavedObjectMeta: SavedObjectMeta; -} - -export interface VisObjectAttributes { - title: string; - visState: string; // JSON string - type: string; - params: any; - uiState: { - vis: { - params: { - sort: { - columnIndex: string; - direction: string; - }; - }; - }; - }; - aggs: any[]; - sort: any[]; - kibanaSavedObjectMeta: SavedObjectMeta; -} - -export interface SavedObjectReference { - name: string; // should be kibanaSavedObjectMeta.searchSourceJSON.index - type: string; // should be index-pattern - id: string; -} - -export interface SavedObject { - attributes: any; - references: SavedObjectReference[]; -} - -/* This object is passed to different helpers in different parts of the code - - packages/kbn-es-query/src/es_query/build_es_query - - x-pack/legacy/plugins/reporting/export_types/csv/server/lib/field_format_map - The structure has redundant parts and json-parsed / json-unparsed versions of the same data - */ -export interface IndexPatternSavedObject { - title: string; - timeFieldName: string; - fields: any[]; - attributes: { - fieldFormatMap: string; - fields: string; - }; -} - -export interface VisPanel { - indexPatternSavedObjectId?: string; - savedSearchObjectId?: string; - attributes: VisObjectAttributes; - timerange: TimeRangeParams; -} - -export interface SearchPanel { - indexPatternSavedObjectId: string; - attributes: SavedSearchObjectAttributes; - timerange: TimeRangeParams; -} - -export interface SearchSourceQuery { - isSearchSourceQuery: boolean; -} - -export interface SearchSource { - query: SearchSourceQuery; - filter: any[]; -} - -/* - * These filter types are stub types to help ensure things get passed to - * non-Typescript functions in the right order. An actual structure is not - * needed because the code doesn't look into the properties; just combines them - * and passes them through to other non-TS modules. - */ -export interface Filter { - isFilter: boolean; -} -export interface TimeFilter extends Filter { - isTimeFilter: boolean; -} -export interface QueryFilter extends Filter { - isQueryFilter: boolean; -} -export interface SearchSourceFilter extends Filter { - isSearchSourceFilter: boolean; -} - -export interface IndexPatternField { - scripted: boolean; - lang?: string; - script?: string; - name: string; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/png/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/index.ts deleted file mode 100644 index 04f56185d4910..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/index.ts +++ /dev/null @@ -1,44 +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 { - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_TRIAL, - PNG_JOB_TYPE as jobType, -} from '../../common/constants'; -import { - ESQueueCreateJobFn, - ESQueueWorkerExecuteFn, - ExportTypeDefinition, -} from '../../server/types'; -import { metadata } from './metadata'; -import { createJobFactory } from './server/create_job'; -import { executeJobFactory } from './server/execute_job'; -import { JobDocPayloadPNG, JobParamsPNG } from './types'; - -export const getExportType = (): ExportTypeDefinition< - JobParamsPNG, - ESQueueCreateJobFn, - JobDocPayloadPNG, - ESQueueWorkerExecuteFn -> => ({ - ...metadata, - jobType, - jobContentEncoding: 'base64', - jobContentExtension: 'PNG', - createJobFactory, - executeJobFactory, - validLicenses: [ - LICENSE_TYPE_TRIAL, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - ], -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts deleted file mode 100644 index ab492c21256eb..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts +++ /dev/null @@ -1,39 +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 { validateUrls } from '../../../../common/validate_urls'; -import { cryptoFactory } from '../../../../server/lib'; -import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../server/types'; -import { JobParamsPNG } from '../../types'; - -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting) { - const config = reporting.getConfig(); - const setupDeps = reporting.getPluginSetupDeps(); - const crypto = cryptoFactory(config.get('encryptionKey')); - - return async function createJob( - { objectType, title, relativeUrl, browserTimezone, layout }, - context, - req - ) { - const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - - validateUrls([relativeUrl]); - - return { - objectType, - title, - relativeUrl, - headers: serializedEncryptedHeaders, - browserTimezone, - layout, - basePath: setupDeps.basePath(req), - forceNow: new Date().toISOString(), - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts deleted file mode 100644 index 7e816e11f1953..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts +++ /dev/null @@ -1,154 +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 * as Rx from 'rxjs'; -import { CancellationToken } from '../../../../../../../plugins/reporting/common'; -import { ReportingCore } from '../../../../server'; -import { cryptoFactory, LevelLogger } from '../../../../server/lib'; -import { createMockReportingCore } from '../../../../test_helpers'; -import { JobDocPayloadPNG } from '../../types'; -import { generatePngObservableFactory } from '../lib/generate_png'; -import { executeJobFactory } from './index'; - -jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); - -let mockReporting: ReportingCore; - -const cancellationToken = ({ - on: jest.fn(), -} as unknown) as CancellationToken; - -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); - -const mockEncryptionKey = 'abcabcsecuresecret'; -const encryptHeaders = async (headers: Record) => { - const crypto = cryptoFactory(mockEncryptionKey); - return await crypto.encrypt(headers); -}; - -const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPNG; - -beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; - const reportingConfig = { - index: '.reporting-2018.10.10', - encryptionKey: mockEncryptionKey, - 'kibanaServer.hostname': 'localhost', - 'kibanaServer.port': 5601, - 'kibanaServer.protocol': 'http', - 'queue.indexInterval': 'daily', - 'queue.timeout': Infinity, - }; - const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, - }; - - mockReporting = await createMockReportingCore(mockReportingConfig); - - const mockElasticsearch = { - legacy: { - client: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), - }, - }, - }; - const mockGetElasticsearch = jest.fn(); - mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); - mockReporting.getElasticsearchService = mockGetElasticsearch; - - (generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn()); -}); - -afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); - -test(`passes browserTimezone to generatePng`, async () => { - const encryptedHeaders = await encryptHeaders({}); - const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const browserTimezone = 'UTC'; - await executeJob( - 'pngJobId', - getJobDocPayload({ - relativeUrl: '/app/kibana#/something', - browserTimezone, - headers: encryptedHeaders, - }), - cancellationToken - ); - - expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - LevelLogger { - "_logger": Object { - "get": [MockFunction], - }, - "_tags": Array [ - "PNG", - "execute", - "pngJobId", - ], - "warning": [Function], - }, - "http://localhost:5601/sbp/app/kibana#/something", - "UTC", - Object { - "conditions": Object { - "basePath": "/sbp", - "hostname": "localhost", - "port": 5601, - "protocol": "http", - }, - "headers": Object {}, - }, - undefined, - ], - ] - `); -}); - -test(`returns content_type of application/png`, async () => { - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = await generatePngObservableFactory(mockReporting); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of('foo')); - - const { content_type: contentType } = await executeJob( - 'pngJobId', - getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), - cancellationToken - ); - expect(contentType).toBe('image/png'); -}); - -test(`returns content of generatePng getBuffer base64 encoded`, async () => { - const testContent = 'raw string from get_screenhots'; - const generatePngObservable = await generatePngObservableFactory(mockReporting); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ base64: testContent })); - - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const encryptedHeaders = await encryptHeaders({}); - const { content } = await executeJob( - 'pngJobId', - getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), - cancellationToken - ); - - expect(content).toEqual(testContent); -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts deleted file mode 100644 index 0d0a9e748682a..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ /dev/null @@ -1,78 +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 apm from 'elastic-apm-node'; -import * as Rx from 'rxjs'; -import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { PNG_JOB_TYPE } from '../../../../common/constants'; -import { ReportingCore } from '../../../../server'; -import { LevelLogger } from '../../../../server/lib'; -import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput } from '../../../../server/types'; -import { - decryptJobHeaders, - getConditionalHeaders, - getFullUrls, - omitBlacklistedHeaders, -} from '../../../common/execute_job/'; -import { JobDocPayloadPNG } from '../../types'; -import { generatePngObservableFactory } from '../lib/generate_png'; - -type QueuedPngExecutorFactory = ExecuteJobFactory>; - -export const executeJobFactory: QueuedPngExecutorFactory = async function executeJobFactoryFn( - reporting: ReportingCore, - parentLogger: LevelLogger -) { - const config = reporting.getConfig(); - const encryptionKey = config.get('encryptionKey'); - const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); - - return async function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { - const apmTrans = apm.startTransaction('reporting execute_job png', 'reporting'); - const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); - let apmGeneratePng: { end: () => void } | null | undefined; - - const generatePngObservable = await generatePngObservableFactory(reporting); - const jobLogger = logger.clone([jobId]); - const process$: Rx.Observable = Rx.of(1).pipe( - mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), - map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), - map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), - mergeMap((conditionalHeaders) => { - const urls = getFullUrls({ config, job }); - const hashUrl = urls[0]; - if (apmGetAssets) apmGetAssets.end(); - - apmGeneratePng = apmTrans?.startSpan('generate_png_pipeline', 'execute'); - return generatePngObservable( - jobLogger, - hashUrl, - job.browserTimezone, - conditionalHeaders, - job.layout - ); - }), - map(({ base64, warnings }) => { - if (apmGeneratePng) apmGeneratePng.end(); - - return { - content_type: 'image/png', - content: base64, - size: (base64 && base64.length) || 0, - - warnings, - }; - }), - catchError((err) => { - jobLogger.error(err); - return Rx.throwError(err); - }) - ); - - const stop$ = Rx.fromEventPattern(cancellationToken.on); - return process$.pipe(takeUntil(stop$)).toPromise(); - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/png/types.d.ts deleted file mode 100644 index 80a2a1899a075..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/types.d.ts +++ /dev/null @@ -1,26 +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 { JobDocPayload } from '../../server/types'; -import { LayoutInstance, LayoutParams } from '../common/layouts'; - -// Job params: structure of incoming user request data -export interface JobParamsPNG { - objectType: string; - title: string; - relativeUrl: string; - browserTimezone: string; - layout: LayoutInstance; -} - -// Job payload: structure of stored job data provided by create_job -export interface JobDocPayloadPNG extends JobDocPayload { - basePath?: string; - browserTimezone: string; - forceNow?: string; - layout: LayoutParams; - relativeUrl: string; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts deleted file mode 100644 index ec537b52244c2..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts +++ /dev/null @@ -1,44 +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 { - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_TRIAL, - PDF_JOB_TYPE as jobType, -} from '../../common/constants'; -import { - ESQueueCreateJobFn, - ESQueueWorkerExecuteFn, - ExportTypeDefinition, -} from '../../server/types'; -import { metadata } from './metadata'; -import { createJobFactory } from './server/create_job'; -import { executeJobFactory } from './server/execute_job'; -import { JobDocPayloadPDF, JobParamsPDF } from './types'; - -export const getExportType = (): ExportTypeDefinition< - JobParamsPDF, - ESQueueCreateJobFn, - JobDocPayloadPDF, - ESQueueWorkerExecuteFn -> => ({ - ...metadata, - jobType, - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - createJobFactory, - executeJobFactory, - validLicenses: [ - LICENSE_TYPE_TRIAL, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - ], -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts deleted file mode 100644 index ef597cfb45f78..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts +++ /dev/null @@ -1,39 +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 { validateUrls } from '../../../../common/validate_urls'; -import { cryptoFactory } from '../../../../server/lib'; -import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../server/types'; -import { JobParamsPDF } from '../../types'; - -export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting) { - const config = reporting.getConfig(); - const setupDeps = reporting.getPluginSetupDeps(); - const crypto = cryptoFactory(config.get('encryptionKey')); - - return async function createJobFn( - { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, - context, - req - ) { - const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - - validateUrls(relativeUrls); - - return { - basePath: setupDeps.basePath(req), - browserTimezone, - forceNow: new Date().toISOString(), - headers: serializedEncryptedHeaders, - layout, - relativeUrls, - title, - objectType, - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts deleted file mode 100644 index fdf9c24730638..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts +++ /dev/null @@ -1,157 +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. - */ - -jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); - -import * as Rx from 'rxjs'; -import { CancellationToken } from '../../../../../../../plugins/reporting/common'; -import { ReportingCore } from '../../../../server'; -import { cryptoFactory, LevelLogger } from '../../../../server/lib'; -import { createMockReportingCore } from '../../../../test_helpers'; -import { JobDocPayloadPDF } from '../../types'; -import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { executeJobFactory } from './index'; - -let mockReporting: ReportingCore; - -const cancellationToken = ({ - on: jest.fn(), -} as unknown) as CancellationToken; - -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); - -const mockEncryptionKey = 'testencryptionkey'; -const encryptHeaders = async (headers: Record) => { - const crypto = cryptoFactory(mockEncryptionKey); - return await crypto.encrypt(headers); -}; - -const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPDF; - -beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; - const reportingConfig = { - index: '.reports-test', - encryptionKey: mockEncryptionKey, - 'kibanaServer.hostname': 'localhost', - 'kibanaServer.port': 5601, - 'kibanaServer.protocol': 'http', - }; - const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, - }; - - mockReporting = await createMockReportingCore(mockReportingConfig); - - const mockElasticsearch = { - legacy: { - client: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), - }, - }, - }; - const mockGetElasticsearch = jest.fn(); - mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); - mockReporting.getElasticsearchService = mockGetElasticsearch; - - (generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn()); -}); - -afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); - -test(`passes browserTimezone to generatePdf`, async () => { - const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const browserTimezone = 'UTC'; - await executeJob( - 'pdfJobId', - getJobDocPayload({ - relativeUrl: '/app/kibana#/something', - browserTimezone, - headers: encryptedHeaders, - }), - cancellationToken - ); - - expect(generatePdfObservable.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - LevelLogger { - "_logger": Object { - "get": [MockFunction], - }, - "_tags": Array [ - "printable_pdf", - "execute", - "pdfJobId", - ], - "warning": [Function], - }, - undefined, - Array [ - "http://localhost:5601/sbp/app/kibana#/something", - ], - "UTC", - Object { - "conditions": Object { - "basePath": "/sbp", - "hostname": "localhost", - "port": 5601, - "protocol": "http", - }, - "headers": Object {}, - }, - undefined, - false, - ], - ] - `); -}); - -test(`returns content_type of application/pdf`, async () => { - const logger = getMockLogger(); - const executeJob = await executeJobFactory(mockReporting, logger); - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = await generatePdfObservableFactory(mockReporting); - (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); - - const { content_type: contentType } = await executeJob( - 'pdfJobId', - getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), - cancellationToken - ); - expect(contentType).toBe('application/pdf'); -}); - -test(`returns content of generatePdf getBuffer base64 encoded`, async () => { - const testContent = 'test content'; - const generatePdfObservable = await generatePdfObservableFactory(mockReporting); - (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const encryptedHeaders = await encryptHeaders({}); - const { content } = await executeJob( - 'pdfJobId', - getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), - cancellationToken - ); - - expect(content).toEqual(Buffer.from(testContent).toString('base64')); -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts deleted file mode 100644 index b0b2d02305b9b..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ /dev/null @@ -1,92 +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 apm from 'elastic-apm-node'; -import * as Rx from 'rxjs'; -import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { PDF_JOB_TYPE } from '../../../../common/constants'; -import { ReportingCore } from '../../../../server'; -import { LevelLogger } from '../../../../server/lib'; -import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput } from '../../../../server/types'; -import { - decryptJobHeaders, - getConditionalHeaders, - getCustomLogo, - getFullUrls, - omitBlacklistedHeaders, -} from '../../../common/execute_job/'; -import { JobDocPayloadPDF } from '../../types'; -import { generatePdfObservableFactory } from '../lib/generate_pdf'; - -type QueuedPdfExecutorFactory = ExecuteJobFactory>; - -export const executeJobFactory: QueuedPdfExecutorFactory = async function executeJobFactoryFn( - reporting: ReportingCore, - parentLogger: LevelLogger -) { - const config = reporting.getConfig(); - const encryptionKey = config.get('encryptionKey'); - - const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']); - - return async function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { - const apmTrans = apm.startTransaction('reporting execute_job pdf', 'reporting'); - const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); - let apmGeneratePdf: { end: () => void } | null | undefined; - - const generatePdfObservable = await generatePdfObservableFactory(reporting); - - const jobLogger = logger.clone([jobId]); - const process$: Rx.Observable = Rx.of(1).pipe( - mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), - map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), - map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), - mergeMap((conditionalHeaders) => - getCustomLogo({ reporting, config, job, conditionalHeaders }) - ), - mergeMap(({ logo, conditionalHeaders }) => { - const urls = getFullUrls({ config, job }); - - const { browserTimezone, layout, title } = job; - if (apmGetAssets) apmGetAssets.end(); - - apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute'); - return generatePdfObservable( - jobLogger, - title, - urls, - browserTimezone, - conditionalHeaders, - layout, - logo - ); - }), - map(({ buffer, warnings }) => { - if (apmGeneratePdf) apmGeneratePdf.end(); - - const apmEncode = apmTrans?.startSpan('encode_pdf', 'output'); - const content = buffer?.toString('base64') || null; - if (apmEncode) apmEncode.end(); - - return { - content_type: 'application/pdf', - content, - size: buffer?.byteLength || 0, - warnings, - }; - }), - catchError((err) => { - jobLogger.error(err); - return Rx.throwError(err); - }) - ); - - const stop$ = Rx.fromEventPattern(cancellationToken.on); - - if (apmTrans) apmTrans.end(); - return process$.pipe(takeUntil(stop$)).toPromise(); - }; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts deleted file mode 100644 index 0df01fdc16d1c..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts +++ /dev/null @@ -1,26 +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 { JobDocPayload } from '../../server/types'; -import { LayoutInstance, LayoutParams } from '../common/layouts'; - -// Job params: structure of incoming user request data, after being parsed from RISON -export interface JobParamsPDF { - objectType: string; // visualization, dashboard, etc. Used for job info & telemetry - title: string; - relativeUrls: string[]; - browserTimezone: string; - layout: LayoutInstance; -} - -// Job payload: structure of stored job data provided by create_job -export interface JobDocPayloadPDF extends JobDocPayload { - basePath?: string; - browserTimezone: string; - forceNow?: string; - layout: LayoutParams; - relativeUrls: string[]; -} diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts deleted file mode 100644 index 1ae971b6566b0..0000000000000 --- a/x-pack/legacy/plugins/reporting/index.ts +++ /dev/null @@ -1,49 +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 { i18n } from '@kbn/i18n'; -import { Legacy } from 'kibana'; -import { resolve } from 'path'; -import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; -import { legacyInit } from './server/legacy'; - -export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; - -const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6); - -export const reporting = (kibana: any) => { - return new kibana.Plugin({ - id: PLUGIN_ID, - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - - uiExports: { - uiSettingDefaults: { - [UI_SETTINGS_CUSTOM_PDF_LOGO]: { - name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { - defaultMessage: 'PDF footer image', - }), - value: null, - description: i18n.translate('xpack.reporting.pdfFooterImageDescription', { - defaultMessage: `Custom image to use in the PDF's footer`, - }), - type: 'image', - validation: { - maxSize: { - length: kbToBase64Length(200), - description: '200 kB', - }, - }, - category: [PLUGIN_ID], - }, - }, - }, - - async init(server: Legacy.Server) { - return legacyInit(server, this); - }, - } as ReportingPluginSpecOptions); -}; diff --git a/x-pack/legacy/plugins/reporting/reporting.d.ts b/x-pack/legacy/plugins/reporting/reporting.d.ts deleted file mode 100644 index ec65c15f53864..0000000000000 --- a/x-pack/legacy/plugins/reporting/reporting.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ReportingPlugin } from './server/plugin'; diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/download.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/download.ts deleted file mode 100644 index a5ad69b46e46d..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/download.ts +++ /dev/null @@ -1,49 +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 { openSync, writeSync, closeSync, mkdirSync } from 'fs'; -import { createHash } from 'crypto'; -import { dirname } from 'path'; - -import Axios from 'axios'; - -import { log } from './util'; - -/** - * Download a url and calculate it's checksum - * @param {String} url - * @param {String} path - * @return {Promise} checksum of the downloaded file - */ -export async function download(url: string, path: string) { - log(`Downloading ${url}`); - - const hash = createHash('md5'); - - mkdirSync(dirname(path), { recursive: true }); - const handle = openSync(path, 'w'); - - try { - const resp = await Axios.request({ - url, - method: 'GET', - responseType: 'stream', - }); - - resp.data.on('data', (chunk: Buffer) => { - writeSync(handle, chunk); - hash.update(chunk); - }); - - await new Promise((resolve, reject) => { - resp.data.on('error', reject).on('end', resolve); - }); - } finally { - closeSync(handle); - } - - return hash.digest('hex'); -} diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts deleted file mode 100644 index f1f609ed5607b..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts +++ /dev/null @@ -1,73 +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 { existsSync } from 'fs'; -import { resolve as resolvePath } from 'path'; -import { BrowserDownload, chromium } from '../'; -import { BROWSER_TYPE } from '../../../common/constants'; -import { md5 } from './checksum'; -import { clean } from './clean'; -import { download } from './download'; -import { asyncMap } from './util'; - -/** - * Check for the downloaded archive of each requested browser type and - * download them if they are missing or their checksum is invalid - * @param {String} browserType - * @return {Promise} - */ -export async function ensureBrowserDownloaded(browserType = BROWSER_TYPE) { - await ensureDownloaded([chromium]); -} - -/** - * Like ensureBrowserDownloaded(), except it applies to all browsers - * @return {Promise} - */ -export async function ensureAllBrowsersDownloaded() { - await ensureDownloaded([chromium]); -} - -/** - * Clears the unexpected files in the browsers archivesPath - * and ensures that all packages/archives are downloaded and - * that their checksums match the declared value - * @param {BrowserSpec} browsers - * @return {Promise} - */ -async function ensureDownloaded(browsers: BrowserDownload[]) { - await asyncMap(browsers, async (browser) => { - const { archivesPath } = browser.paths; - - await clean( - archivesPath, - browser.paths.packages.map((p) => resolvePath(archivesPath, p.archiveFilename)) - ); - - const invalidChecksums: string[] = []; - await asyncMap(browser.paths.packages, async ({ archiveFilename, archiveChecksum }) => { - const url = `${browser.paths.baseUrl}${archiveFilename}`; - const path = resolvePath(archivesPath, archiveFilename); - - if (existsSync(path) && (await md5(path)) === archiveChecksum) { - return; - } - - const downloadedChecksum = await download(url, path); - if (downloadedChecksum !== archiveChecksum) { - invalidChecksums.push(`${url} => ${path}`); - } - }); - - if (invalidChecksums.length) { - throw new Error( - `Error downloading browsers, checksums incorrect for:\n - ${invalidChecksums.join( - '\n - ' - )}` - ); - } - }); -} diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/util.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/util.ts deleted file mode 100644 index 679106742e3d0..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/util.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Readable } from 'stream'; - -/** - * Log a message if the DEBUG environment variable is set - */ -export function log(...args: any[]) { - if (process.env.DEBUG) { - // allow console log since this is off by default and only for debugging - // eslint-disable-next-line no-console - console.log(...args); - } -} - -/** - * Iterate an array asynchronously and in parallel - */ -export function asyncMap(array: T[], asyncFn: (x: T) => T2): Promise { - return Promise.all(array.map(asyncFn)); -} - -/** - * Wait for a readable stream to end - */ -export function readableEnd(stream: Readable) { - return new Promise((resolve, reject) => { - stream.on('error', reject).on('end', resolve); - }); -} diff --git a/x-pack/legacy/plugins/reporting/server/config/index.ts b/x-pack/legacy/plugins/reporting/server/config/index.ts deleted file mode 100644 index 3ec5aab4d451b..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/config/index.ts +++ /dev/null @@ -1,88 +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 { Legacy } from 'kibana'; -import { get } from 'lodash'; -import { CoreSetup } from 'src/core/server'; -import { ConfigType as ReportingConfigType } from '../../../../../plugins/reporting/server'; - -// make config.get() aware of the value type it returns -interface Config { - get(key1: Key1): BaseType[Key1]; - get( - key1: Key1, - key2: Key2 - ): BaseType[Key1][Key2]; - get< - Key1 extends keyof BaseType, - Key2 extends keyof BaseType[Key1], - Key3 extends keyof BaseType[Key1][Key2] - >( - key1: Key1, - key2: Key2, - key3: Key3 - ): BaseType[Key1][Key2][Key3]; - get< - Key1 extends keyof BaseType, - Key2 extends keyof BaseType[Key1], - Key3 extends keyof BaseType[Key1][Key2], - Key4 extends keyof BaseType[Key1][Key2][Key3] - >( - key1: Key1, - key2: Key2, - key3: Key3, - key4: Key4 - ): BaseType[Key1][Key2][Key3][Key4]; -} - -interface KbnServerConfigType { - path: { data: string }; - server: { - basePath: string; - host: string; - name: string; - port: number; - protocol: string; - uuid: string; - }; -} - -export interface ReportingConfig extends Config { - kbnConfig: Config; -} - -export const buildConfig = ( - core: CoreSetup, - server: Legacy.Server, - reportingConfig: ReportingConfigType -): ReportingConfig => { - const config = server.config(); - const { http } = core; - const serverInfo = http.getServerInfo(); - - const kbnConfig = { - path: { - data: config.get('path.data'), - }, - server: { - basePath: core.http.basePath.serverBasePath, - host: serverInfo.host, - name: serverInfo.name, - port: serverInfo.port, - uuid: core.uuid.getInstanceUuid(), - protocol: serverInfo.protocol, - }, - }; - - return { - get: (...keys: string[]) => get(reportingConfig, keys.join('.'), null), // spreading arguments as an array allows the return type to be known by the compiler - kbnConfig: { - get: (...keys: string[]) => get(kbnConfig, keys.join('.'), null), - }, - }; -}; - -export { ReportingConfigType }; diff --git a/x-pack/legacy/plugins/reporting/server/index.ts b/x-pack/legacy/plugins/reporting/server/index.ts deleted file mode 100644 index 2388eac48f8cc..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/index.ts +++ /dev/null @@ -1,17 +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 { PluginInitializerContext } from 'src/core/server'; -import { ReportingConfig } from './config'; -import { ReportingCore } from './core'; -import { ReportingPlugin as Plugin } from './plugin'; - -export const plugin = (context: PluginInitializerContext, config: ReportingConfig) => { - return new Plugin(context, config); -}; - -export { ReportingPlugin } from './plugin'; -export { ReportingConfig, ReportingCore }; diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts deleted file mode 100644 index 14abd53cc83d9..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/legacy.ts +++ /dev/null @@ -1,61 +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 { Legacy } from 'kibana'; -import { take } from 'rxjs/operators'; -import { PluginInitializerContext } from 'src/core/server'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; -import { ReportingPluginSpecOptions } from '../'; -import { PluginsSetup } from '../../../../plugins/reporting/server'; -import { SecurityPluginSetup } from '../../../../plugins/security/server'; -import { buildConfig } from './config'; -import { plugin } from './index'; -import { LegacySetup, ReportingStartDeps } from './types'; - -const buildLegacyDependencies = ( - server: Legacy.Server, - reportingPlugin: ReportingPluginSpecOptions -): LegacySetup => ({ - route: server.route.bind(server), - plugins: { - xpack_main: server.plugins.xpack_main, - reporting: reportingPlugin, - }, -}); - -/* - * Starts the New Platform instance of Reporting using legacy dependencies - */ -export const legacyInit = async ( - server: Legacy.Server, - reportingLegacyPlugin: ReportingPluginSpecOptions -) => { - const { core: coreSetup } = server.newPlatform.setup; - const { config$ } = (server.newPlatform.setup.plugins.reporting as PluginsSetup).__legacy; - const reportingConfig = await config$.pipe(take(1)).toPromise(); - const __LEGACY = buildLegacyDependencies(server, reportingLegacyPlugin); - - const pluginInstance = plugin( - server.newPlatform.coreContext as PluginInitializerContext, - buildConfig(coreSetup, server, reportingConfig) - ); - - await pluginInstance.setup(coreSetup, { - elasticsearch: coreSetup.elasticsearch, - licensing: server.newPlatform.setup.plugins.licensing as LicensingPluginSetup, - security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, - usageCollection: server.newPlatform.setup.plugins.usageCollection, - __LEGACY, - }); - - // Schedule to call the "start" hook only after start dependencies are ready - coreSetup.getStartServices().then(([core, plugins]) => - pluginInstance.start(core, { - data: (plugins as ReportingStartDeps).data, - __LEGACY, - }) - ); -}; diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/job.js deleted file mode 100644 index 2d0ac86fd96f1..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/job.js +++ /dev/null @@ -1,17 +0,0 @@ -import events from 'events'; - -export class JobMock extends events.EventEmitter { - constructor(queue, index, type, payload, options = {}) { - super(); - - this.queue = queue; - this.index = index; - this.jobType = type; - this.payload = payload; - this.options = options; - } - - getProp(name) { - return this[name]; - } -} diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/queue.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/queue.js deleted file mode 100644 index 6bdd922555fa9..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/queue.js +++ /dev/null @@ -1,11 +0,0 @@ -import events from 'events'; - -export class QueueMock extends events.EventEmitter { - constructor() { - super(); - } - - setClient(client) { - this.client = client; - } -} diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/worker.js deleted file mode 100644 index 09c94b44a8145..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/worker.js +++ /dev/null @@ -1,16 +0,0 @@ -import events from 'events'; - -export class WorkerMock extends events.EventEmitter { - constructor(queue, type, workerFn, opts = {}) { - super(); - - this.queue = queue; - this.type = type; - this.workerFn = workerFn; - this.options = opts; - } - - getProp(name) { - return this[name]; - } -} diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js deleted file mode 100644 index f852ac9c92404..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ /dev/null @@ -1,464 +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 events from 'events'; -import moment from 'moment'; -import Puid from 'puid'; -import { CancellationToken } from '../../../../../../plugins/reporting/common'; -import { Poller } from '../../../../../common/poller'; -import { constants } from './constants'; -import { UnspecifiedWorkerError, WorkerTimeoutError } from './helpers/errors'; - -const puid = new Puid(); - -export function formatJobObject(job) { - return { - index: job._index, - id: job._id, - }; -} - -export function getUpdatedDocPath(response) { - const { _index: ind, _id: id } = response; - return `/${ind}/${id}`; -} - -const MAX_PARTIAL_ERROR_LENGTH = 1000; // 1000 of beginning, 1000 of end -const ERROR_PARTIAL_SEPARATOR = '...'; -const MAX_ERROR_LENGTH = MAX_PARTIAL_ERROR_LENGTH * 2 + ERROR_PARTIAL_SEPARATOR.length; - -function getLogger(opts, id, logLevel) { - return (msg, err) => { - /* - * This does not get the logger instance from queue.registerWorker in the createWorker function. - * The logger instance in the Equeue lib comes from createTaggedLogger, so logLevel tags are passed differently - */ - const logger = opts.logger || function () {}; - const message = `${id} - ${msg}`; - const tags = [logLevel]; - - if (err) { - // The error message string could be very long if it contains the request - // body of a request that was too large for Elasticsearch. - // This takes a partial version of the error message without scanning - // every character of the string, which would block Node. - const errString = `${message}: ${err.stack ? err.stack : err}`; - const errLength = errString.length; - const subStr = String.prototype.substring.bind(errString); - if (errLength > MAX_ERROR_LENGTH) { - const partialError = - subStr(0, MAX_PARTIAL_ERROR_LENGTH) + - ERROR_PARTIAL_SEPARATOR + - subStr(errLength - MAX_PARTIAL_ERROR_LENGTH); - - logger(partialError, tags); - logger( - `A partial version of the entire error message was logged. ` + - `The entire error message length is: ${errLength} characters.`, - tags - ); - } else { - logger(errString, tags); - } - return; - } - - logger(message, tags); - }; -} - -export class Worker extends events.EventEmitter { - constructor(queue, type, workerFn, opts) { - if (typeof type !== 'string') throw new Error('type must be a string'); - if (typeof workerFn !== 'function') throw new Error('workerFn must be a function'); - if (typeof opts !== 'object') throw new Error('opts must be an object'); - if (typeof opts.interval !== 'number') throw new Error('opts.interval must be a number'); - if (typeof opts.intervalErrorMultiplier !== 'number') - throw new Error('opts.intervalErrorMultiplier must be a number'); - - super(); - - this.id = puid.generate(); - this.kibanaId = opts.kibanaId; - this.kibanaName = opts.kibanaName; - this.queue = queue; - this._client = this.queue.client; - this.jobtype = type; - this.workerFn = workerFn; - - this.debug = getLogger(opts, this.id, 'debug'); - this.warn = getLogger(opts, this.id, 'warning'); - this.error = getLogger(opts, this.id, 'error'); - this.info = getLogger(opts, this.id, 'info'); - - this._running = true; - this.debug(`Created worker for ${this.jobtype} jobs`); - - this._poller = new Poller({ - functionToPoll: () => { - return this._processPendingJobs(); - }, - pollFrequencyInMillis: opts.interval, - trailing: true, - continuePollingOnError: true, - pollFrequencyErrorMultiplier: opts.intervalErrorMultiplier, - }); - this._startJobPolling(); - } - - destroy() { - this._running = false; - this._stopJobPolling(); - } - - toJSON() { - return { - id: this.id, - index: this.queue.index, - jobType: this.jobType, - }; - } - - emit(name, ...args) { - super.emit(name, ...args); - this.queue.emit(name, ...args); - } - - _formatErrorParams(err, job) { - const response = { - error: err, - worker: this.toJSON(), - }; - - if (job) response.job = formatJobObject(job); - return response; - } - - _claimJob(job) { - const m = moment(); - const startTime = m.toISOString(); - const expirationTime = m.add(job._source.timeout).toISOString(); - const attempts = job._source.attempts + 1; - - if (attempts > job._source.max_attempts) { - const msg = !job._source.output - ? `Max attempts reached (${job._source.max_attempts})` - : false; - return this._failJob(job, msg).then(() => false); - } - - const doc = { - attempts: attempts, - started_at: startTime, - process_expiration: expirationTime, - status: constants.JOB_STATUS_PROCESSING, - kibana_id: this.kibanaId, - kibana_name: this.kibanaName, - }; - - return this._client - .callAsInternalUser('update', { - index: job._index, - id: job._id, - if_seq_no: job._seq_no, - if_primary_term: job._primary_term, - body: { doc }, - }) - .then((response) => { - this.info(`Job marked as claimed: ${getUpdatedDocPath(response)}`); - const updatedJob = { - ...job, - ...response, - }; - updatedJob._source = { - ...job._source, - ...doc, - }; - return updatedJob; - }); - } - - _failJob(job, output = false) { - this.warn(`Failing job ${job._id}`); - - const completedTime = moment().toISOString(); - const docOutput = this._formatOutput(output); - const doc = { - status: constants.JOB_STATUS_FAILED, - completed_at: completedTime, - output: docOutput, - }; - - this.emit(constants.EVENT_WORKER_JOB_FAIL, { - job: formatJobObject(job), - worker: this.toJSON(), - output: docOutput, - }); - - return this._client - .callAsInternalUser('update', { - index: job._index, - id: job._id, - if_seq_no: job._seq_no, - if_primary_term: job._primary_term, - body: { doc }, - }) - .then((response) => { - this.info(`Job marked as failed: ${getUpdatedDocPath(response)}`); - }) - .catch((err) => { - if (err.statusCode === 409) return true; - this.error(`_failJob failed to update job ${job._id}`, err); - this.emit(constants.EVENT_WORKER_FAIL_UPDATE_ERROR, this._formatErrorParams(err, job)); - return false; - }); - } - - _formatOutput(output) { - const unknownMime = false; - const defaultOutput = null; - const docOutput = {}; - - if (typeof output === 'object' && output.content) { - docOutput.content = output.content; - docOutput.content_type = output.content_type || unknownMime; - docOutput.max_size_reached = output.max_size_reached; - docOutput.csv_contains_formulas = output.csv_contains_formulas; - docOutput.size = output.size; - docOutput.warnings = - output.warnings && output.warnings.length > 0 ? output.warnings : undefined; - } else { - docOutput.content = output || defaultOutput; - docOutput.content_type = unknownMime; - } - - return docOutput; - } - - _performJob(job) { - this.info(`Starting job`); - - const workerOutput = new Promise((resolve, reject) => { - // run the worker's workerFn - let isResolved = false; - const cancellationToken = new CancellationToken(); - const jobSource = job._source; - - Promise.resolve(this.workerFn.call(null, job, jobSource.payload, cancellationToken)) - .then((res) => { - // job execution was successful - if (res && res.warnings && res.warnings.length > 0) { - this.warn(`Job execution completed with warnings`); - } else { - this.info(`Job execution completed successfully`); - } - - isResolved = true; - resolve(res); - }) - .catch((err) => { - isResolved = true; - reject(err); - }); - - // fail if workerFn doesn't finish before timeout - const { timeout } = jobSource; - setTimeout(() => { - if (isResolved) return; - - cancellationToken.cancel(); - this.warn(`Timeout processing job ${job._id}`); - reject( - new WorkerTimeoutError(`Worker timed out, timeout = ${timeout}`, { - jobId: job._id, - timeout, - }) - ); - }, timeout); - }); - - return workerOutput.then( - (output) => { - const completedTime = moment().toISOString(); - const docOutput = this._formatOutput(output); - - const status = - output && output.warnings && output.warnings.length > 0 - ? constants.JOB_STATUS_WARNINGS - : constants.JOB_STATUS_COMPLETED; - const doc = { - status, - completed_at: completedTime, - output: docOutput, - }; - - return this._client - .callAsInternalUser('update', { - index: job._index, - id: job._id, - if_seq_no: job._seq_no, - if_primary_term: job._primary_term, - body: { doc }, - }) - .then((response) => { - const eventOutput = { - job: formatJobObject(job), - output: docOutput, - }; - this.emit(constants.EVENT_WORKER_COMPLETE, eventOutput); - - this.info(`Job data saved successfully: ${getUpdatedDocPath(response)}`); - }) - .catch((err) => { - if (err.statusCode === 409) return false; - this.error(`Failure saving job output ${job._id}`, err); - this.emit(constants.EVENT_WORKER_JOB_UPDATE_ERROR, this._formatErrorParams(err, job)); - return this._failJob(job, err.message ? err.message : false); - }); - }, - (jobErr) => { - if (!jobErr) { - jobErr = new UnspecifiedWorkerError('Unspecified worker error', { - jobId: job._id, - }); - } - - // job execution failed - if (jobErr.name === 'WorkerTimeoutError') { - this.warn(`Timeout on job ${job._id}`); - this.emit(constants.EVENT_WORKER_JOB_TIMEOUT, this._formatErrorParams(jobErr, job)); - return; - - // append the jobId to the error - } else { - try { - Object.assign(jobErr, { jobId: job._id }); - } catch (e) { - // do nothing if jobId can not be appended - } - } - - this.error(`Failure occurred on job ${job._id}`, jobErr); - this.emit(constants.EVENT_WORKER_JOB_EXECUTION_ERROR, this._formatErrorParams(jobErr, job)); - return this._failJob(job, jobErr.toString ? jobErr.toString() : false); - } - ); - } - - _startJobPolling() { - if (!this._running) { - return; - } - - this._poller.start(); - } - - _stopJobPolling() { - this._poller.stop(); - } - - _processPendingJobs() { - return this._getPendingJobs().then((jobs) => { - return this._claimPendingJobs(jobs); - }); - } - - _claimPendingJobs(jobs) { - if (!jobs || jobs.length === 0) return; - - let claimed = false; - - // claim a single job, stopping after first successful claim - return jobs - .reduce((chain, job) => { - return chain.then((claimedJob) => { - // short-circuit the promise chain if a job has been claimed - if (claimed) return claimedJob; - - return this._claimJob(job) - .then((claimResult) => { - claimed = true; - return claimResult; - }) - .catch((err) => { - if (err.statusCode === 409) { - this.warn( - `_claimPendingJobs encountered a version conflict on updating pending job ${job._id}`, - err - ); - return; // continue reducing and looking for a different job to claim - } - this.emit(constants.EVENT_WORKER_JOB_CLAIM_ERROR, this._formatErrorParams(err, job)); - return Promise.reject(err); - }); - }); - }, Promise.resolve()) - .then((claimedJob) => { - if (!claimedJob) { - this.debug(`Found no claimable jobs out of ${jobs.length} total`); - return; - } - return this._performJob(claimedJob); - }) - .catch((err) => { - this.error('Error claiming jobs', err); - return Promise.reject(err); - }); - } - - _getPendingJobs() { - const nowTime = moment().toISOString(); - const query = { - seq_no_primary_term: true, - _source: { - excludes: ['output.content'], - }, - query: { - bool: { - filter: { - bool: { - minimum_should_match: 1, - should: [ - { term: { status: 'pending' } }, - { - bool: { - must: [ - { term: { status: 'processing' } }, - { range: { process_expiration: { lte: nowTime } } }, - ], - }, - }, - ], - }, - }, - }, - }, - sort: [{ priority: { order: 'asc' } }, { created_at: { order: 'asc' } }], - size: constants.DEFAULT_WORKER_CHECK_SIZE, - }; - - return this._client - .callAsInternalUser('search', { - index: `${this.queue.index}-*`, - body: query, - }) - .then((results) => { - const jobs = results.hits.hits; - if (jobs.length > 0) { - this.debug(`${jobs.length} outstanding jobs returned`); - } - return jobs; - }) - .catch((err) => { - // ignore missing indices errors - if (err && err.status === 404) return []; - - this.error('job querying failed', err); - this.emit(constants.EVENT_WORKER_JOB_SEARCH_ERROR, this._formatErrorParams(err)); - throw err; - }); - } -} diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts deleted file mode 100644 index 164ffc5742d04..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SecurityPluginSetup } from '../../../../../plugins/security/server'; -import { KibanaRequest } from '../../../../../../src/core/server'; - -export function getUserFactory(security?: SecurityPluginSetup) { - return (request: KibanaRequest) => { - return security?.authc.getCurrentUser(request) ?? null; - }; -} diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts deleted file mode 100644 index 5a407ad3e4c4a..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ /dev/null @@ -1,81 +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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; -import { createBrowserDriverFactory } from './browsers'; -import { ReportingConfig } from './config'; -import { ReportingCore } from './core'; -import { registerRoutes } from './routes'; -import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; -import { setFieldFormats } from './services'; -import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; -import { registerReportingUsageCollector } from './usage'; -// @ts-ignore no module definition -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; - -export class ReportingPlugin - implements Plugin { - private config: ReportingConfig; - private logger: LevelLogger; - private reportingCore: ReportingCore; - - constructor(context: PluginInitializerContext, config: ReportingConfig) { - this.config = config; - this.logger = new LevelLogger(context.logger.get('reporting')); - this.reportingCore = new ReportingCore(this.config); - } - - public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { - const { config } = this; - const { elasticsearch, __LEGACY, licensing, security } = plugins; - const router = core.http.createRouter(); - const basePath = core.http.basePath.get; - const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; - - // legacy plugin status - mirrorPluginStatus(xpackMainLegacy, reportingLegacy); - - const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); - const deps = { - browserDriverFactory, - elasticsearch, - licensing, - basePath, - router, - security, - }; - - runValidations(config, elasticsearch, browserDriverFactory, this.logger); - - this.reportingCore.pluginSetup(deps); - registerReportingUsageCollector(this.reportingCore, plugins); - registerRoutes(this.reportingCore, this.logger); - - return {}; - } - - public async start(core: CoreStart, plugins: ReportingStartDeps) { - const { reportingCore, logger } = this; - - const esqueue = await createQueueFactory(reportingCore, logger); - const enqueueJob = enqueueJobFactory(reportingCore, logger); - - this.reportingCore.pluginStart({ - savedObjects: core.savedObjects, - uiSettings: core.uiSettings, - esqueue, - enqueueJob, - }); - - setFieldFormats(plugins.data.fieldFormats); - - return {}; - } - - public getReportingCore() { - return this.reportingCore; - } -} diff --git a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts deleted file mode 100644 index afa3fd3358fc1..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { KibanaResponseFactory, KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { AuthenticatedUser } from '../../../../../plugins/security/common/model/authenticated_user'; -import { JobDocPayload } from '../types'; - -export type HandlerFunction = ( - user: AuthenticatedUser | null, - exportType: string, - jobParams: object, - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory -) => any; - -export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any; - -export interface QueuedJobPayload { - error?: boolean; - source: { - job: { - payload: JobDocPayload; - }; - }; -} diff --git a/x-pack/legacy/plugins/reporting/server/services.ts b/x-pack/legacy/plugins/reporting/server/services.ts deleted file mode 100644 index 7d15d2e1af1ae..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/services.ts +++ /dev/null @@ -1,12 +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 { createGetterSetter } from '../../../../../src/plugins/kibana_utils/server'; -import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; - -export const [getFieldFormats, setFieldFormats] = createGetterSetter< - DataPluginStart['fieldFormats'] ->('FieldFormats'); diff --git a/x-pack/legacy/plugins/reporting/server/types.ts b/x-pack/legacy/plugins/reporting/server/types.ts deleted file mode 100644 index 2ccc209c3ce50..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/types.ts +++ /dev/null @@ -1,234 +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 { Legacy } from 'kibana'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { ElasticsearchServiceSetup } from 'kibana/server'; -import * as Rx from 'rxjs'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DataPluginStart } from 'src/plugins/data/server/plugin'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; -import { ReportingPluginSpecOptions } from '../'; -import { CancellationToken } from '../../../../plugins/reporting/common'; -import { JobStatus } from '../../../../plugins/reporting/common/types'; -import { SecurityPluginSetup } from '../../../../plugins/security/server'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; -import { LayoutInstance } from '../export_types/common/layouts'; -import { ReportingConfigType } from './config'; -import { ReportingCore } from './core'; -import { LevelLogger } from './lib'; - -/* - * Routing / API types - */ - -interface ListQuery { - page: string; - size: string; - ids?: string; // optional field forbids us from extending RequestQuery -} - -interface GenerateQuery { - jobParams: string; -} - -export type ReportingRequestQuery = ListQuery | GenerateQuery; - -export interface ReportingRequestPre { - management: { - jobTypes: any; - }; - user: string; -} - -// generate a report with unparsed jobParams -export interface GenerateExportTypePayload { - jobParams: string; -} - -export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPayload; - -export interface TimeRangeParams { - timezone: string; - min: Date | string | number | null; - max: Date | string | number | null; -} - -export interface JobParamPostPayload { - timerange: TimeRangeParams; -} - -export interface JobDocPayload { - headers?: string; // serialized encrypted headers - jobParams: JobParamsType; - title: string; - type: string | null; -} - -export interface JobSource { - _id: string; - _index: string; - _source: { - jobtype: string; - output: JobDocOutput; - payload: JobDocPayload; - status: JobStatus; - }; -} - -export interface JobDocOutput { - content_type: string; - content: string | null; - size: number; - max_size_reached?: boolean; - warnings?: string[]; -} - -interface ConditionalHeadersConditions { - protocol: string; - hostname: string; - port: number; - basePath: string; -} - -export interface ConditionalHeaders { - headers: Record; - conditions: ConditionalHeadersConditions; -} - -/* - * Screenshots - */ - -export interface ScreenshotObservableOpts { - logger: LevelLogger; - urls: string[]; - conditionalHeaders: ConditionalHeaders; - layout: LayoutInstance; - browserTimezone: string; -} - -export interface AttributesMap { - [key: string]: any; -} - -export interface ElementPosition { - boundingClientRect: { - // modern browsers support x/y, but older ones don't - top: number; - left: number; - width: number; - height: number; - }; - scroll: { - x: number; - y: number; - }; -} - -export interface ElementsPositionAndAttribute { - position: ElementPosition; - attributes: AttributesMap; -} - -export interface Screenshot { - base64EncodedData: string; - title: string; - description: string; -} - -export interface ScreenshotResults { - timeRange: string | null; - screenshots: Screenshot[]; - error?: Error; - elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing -} - -export type ScreenshotsObservableFn = ({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, -}: ScreenshotObservableOpts) => Rx.Observable; - -/* - * Plugin Contract - */ - -export interface ReportingSetupDeps { - elasticsearch: ElasticsearchServiceSetup; - licensing: LicensingPluginSetup; - security: SecurityPluginSetup; - usageCollection?: UsageCollectionSetup; - __LEGACY: LegacySetup; -} - -export interface ReportingStartDeps { - data: DataPluginStart; - __LEGACY: LegacySetup; -} - -export type ReportingStart = object; -export type ReportingSetup = object; - -export interface LegacySetup { - plugins: { - xpack_main: XPackMainPlugin & { - status?: any; - }; - reporting: ReportingPluginSpecOptions; - }; - route: Legacy.Server['route']; -} - -/* - * Internal Types - */ - -export type ESQueueCreateJobFn = ( - jobParams: JobParamsType, - context: RequestHandlerContext, - request: KibanaRequest -) => Promise; - -export type ESQueueWorkerExecuteFn = ( - jobId: string, - job: JobDocPayloadType, - cancellationToken?: CancellationToken -) => Promise; - -export type ServerFacade = LegacySetup; - -export type CaptureConfig = ReportingConfigType['capture']; -export type ScrollConfig = ReportingConfigType['csv']['scroll']; - -export type CreateJobFactory = ( - reporting: ReportingCore, - logger: LevelLogger -) => CreateJobFnType; - -export type ExecuteJobFactory = ( - reporting: ReportingCore, - logger: LevelLogger -) => Promise; // FIXME: does not "need" to be async - -export interface ExportTypeDefinition< - JobParamsType, - CreateJobFnType, - JobPayloadType, - ExecuteJobFnType -> { - id: string; - name: string; - jobType: string; - jobContentEncoding?: string; - jobContentExtension: string; - createJobFactory: CreateJobFactory; - executeJobFactory: ExecuteJobFactory; - validLicenses: string[]; -} diff --git a/x-pack/legacy/plugins/reporting/server/usage/index.ts b/x-pack/legacy/plugins/reporting/server/usage/index.ts deleted file mode 100644 index d00a8570a024f..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/usage/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LicenseType } from '../../../../../plugins/licensing/server'; - -export interface FeaturesAvailability { - isAvailable: () => boolean; - license: { - getType: () => LicenseType | undefined; - }; -} -export type GetLicense = () => Promise; -export { registerReportingUsageCollector } from './reporting_usage_collector'; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts deleted file mode 100644 index f6dbccdfe3980..0000000000000 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts +++ /dev/null @@ -1,70 +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. - */ - -jest.mock('../server/routes'); -jest.mock('../server/usage'); -jest.mock('../server/browsers'); -jest.mock('../server/browsers'); -jest.mock('../server/lib/create_queue'); -jest.mock('../server/lib/enqueue_job'); -jest.mock('../server/lib/validate'); - -import { of } from 'rxjs'; -import { EventEmitter } from 'events'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { coreMock } from 'src/core/server/mocks'; -import { ReportingConfig, ReportingCore, ReportingPlugin } from '../server'; -import { ReportingSetupDeps, ReportingStartDeps } from '../server/types'; -import { ReportingInternalSetup } from '../server/core'; - -const createMockSetupDeps = (setupMock?: any): ReportingSetupDeps => { - return { - elasticsearch: setupMock.elasticsearch, - security: setupMock.security, - licensing: { - license$: of({ isAvailable: true, isActive: true, type: 'basic' }), - } as any, - usageCollection: {} as any, - __LEGACY: { plugins: { xpack_main: { status: new EventEmitter() } } } as any, - }; -}; - -export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({ - data: startMock.data, - __LEGACY: {} as any, -}); - -const createMockReportingPlugin = async (config: ReportingConfig): Promise => { - config = config || {}; - const plugin = new ReportingPlugin(coreMock.createPluginInitializerContext(config), config); - const setupMock = coreMock.createSetup(); - const coreStartMock = coreMock.createStart(); - const startMock = { - ...coreStartMock, - data: { fieldFormats: {} }, - }; - - await plugin.setup(setupMock, createMockSetupDeps(setupMock)); - await plugin.start(startMock, createMockStartDeps(startMock)); - - return plugin; -}; - -export const createMockReportingCore = async ( - config: ReportingConfig, - setupDepsMock?: ReportingInternalSetup -): Promise => { - config = config || {}; - const plugin = await createMockReportingPlugin(config); - const core = plugin.getReportingCore(); - - if (setupDepsMock) { - // @ts-ignore overwriting private properties - core.pluginSetupDeps = setupDepsMock; - } - - return core; -}; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts index 41371fcbc4c65..addeef34f63bf 100644 --- a/x-pack/legacy/plugins/security/index.ts +++ b/x-pack/legacy/plugins/security/index.ts @@ -6,64 +6,17 @@ import { Root } from 'joi'; import { resolve } from 'path'; -import { Server } from 'src/legacy/server/kbn_server'; -import { KibanaRequest, LegacyRequest } from '../../../../src/core/server'; -// @ts-ignore -import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; -import { AuthenticatedUser, SecurityPluginSetup } from '../../../plugins/security/server'; - -/** - * Public interface of the security plugin. - */ -export interface SecurityPlugin { - getUser: (request: LegacyRequest) => Promise; -} - -function getSecurityPluginSetup(server: Server) { - const securityPlugin = server.newPlatform.setup.plugins.security as SecurityPluginSetup; - if (!securityPlugin) { - throw new Error('Kibana Platform Security plugin is not available.'); - } - - return securityPlugin; -} export const security = (kibana: Record) => new kibana.Plugin({ id: 'security', publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'xpack_main'], + require: ['kibana'], configPrefix: 'xpack.security', - uiExports: { - hacks: ['plugins/security/hacks/legacy'], - injectDefaultVars: (server: Server) => { - return { enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled') }; - }, - }, - - config(Joi: Root) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }) + uiExports: { hacks: ['plugins/security/hacks/legacy'] }, + config: (Joi: Root) => + Joi.object({ enabled: Joi.boolean().default(true) }) .unknown() - .default(); - }, - - async postInit(server: Server) { - watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => { - const xpackInfo = server.plugins.xpack_main.info; - if (xpackInfo.isAvailable() && xpackInfo.feature('security').isEnabled()) { - await getSecurityPluginSetup(server).__legacyCompat.registerPrivilegesWithCluster(); - } - }); - }, - - async init(server: Server) { - const securityPlugin = getSecurityPluginSetup(server); - - server.expose({ - getUser: async (request: LegacyRequest) => - securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)), - }); - }, + .default(), + init() {}, }); diff --git a/x-pack/package.json b/x-pack/package.json index 9f69cbc40bf33..c46d364e0ac46 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -194,7 +194,7 @@ "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.1.1", - "@elastic/numeral": "2.4.0", + "@elastic/numeral": "^2.5.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/i18n": "1.0.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index 459e9d2b03f92..992b2cb16fb06 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -21,6 +21,7 @@ import { ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, } from './schema'; +import { LicenseType } from '../../../../../legacy/common/constants'; export interface AnyParams { [index: string]: string | number | object | undefined | null; @@ -51,6 +52,7 @@ export type Comment = TypeOf; export interface ExternalServiceConfiguration { id: string; name: string; + minimumLicenseRequired: LicenseType; } export interface ExternalServiceCredentials { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 315d13b5aa773..dd8d971b7df44 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -120,9 +120,7 @@ export const createConnector = ({ configurationUtilities, executor = createConnectorExecutor({ api, createExternalService }), }: CreateActionTypeArgs): ActionType => ({ - id: config.id, - name: config.name, - minimumLicenseRequired: 'platinum', + ...config, validate: { config: schema.object(validationSchema.config, { validate: curry(validate.config)(configurationUtilities), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts index 7e415109f1bd9..54f28e447010a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts @@ -10,4 +10,5 @@ import * as i18n from './translations'; export const config: ExternalServiceConfiguration = { id: '.jira', name: i18n.NAME, + minimumLicenseRequired: 'gold', }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts index 4ad8108c3b137..70d53ab79f631 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -10,4 +10,5 @@ import * as i18n from './translations'; export const config: ExternalServiceConfiguration = { id: '.servicenow', name: i18n.NAME, + minimumLicenseRequired: 'platinum', }; diff --git a/x-pack/plugins/advanced_ui_actions/kibana.json b/x-pack/plugins/advanced_ui_actions/kibana.json deleted file mode 100644 index 45907e2d8b602..0000000000000 --- a/x-pack/plugins/advanced_ui_actions/kibana.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "advancedUiActions", - "version": "kibana", - "configPath": ["xpack", "advanced_ui_actions"], - "requiredPlugins": [ - "embeddable", - "uiActions" - ], - "server": false, - "ui": true -} diff --git a/x-pack/plugins/advanced_ui_actions/public/index.ts b/x-pack/plugins/advanced_ui_actions/public/index.ts deleted file mode 100644 index 024cfe5530b97..0000000000000 --- a/x-pack/plugins/advanced_ui_actions/public/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginInitializerContext } from '../../../../src/core/public'; -import { AdvancedUiActionsPublicPlugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new AdvancedUiActionsPublicPlugin(initializerContext); -} - -export { AdvancedUiActionsPublicPlugin as Plugin }; -export { - SetupContract as AdvancedUiActionsSetup, - StartContract as AdvancedUiActionsStart, -} from './plugin'; - -export { ActionWizard } from './components'; -export { - ActionFactoryDefinition as AdvancedUiActionsActionFactoryDefinition, - ActionFactory as AdvancedUiActionsActionFactory, - SerializedAction as UiActionsEnhancedSerializedAction, - SerializedEvent as UiActionsEnhancedSerializedEvent, - AbstractActionStorage as UiActionsEnhancedAbstractActionStorage, - DynamicActionManager as UiActionsEnhancedDynamicActionManager, - DynamicActionManagerParams as UiActionsEnhancedDynamicActionManagerParams, - DynamicActionManagerState as UiActionsEnhancedDynamicActionManagerState, - MemoryActionStorage as UiActionsEnhancedMemoryActionStorage, -} from './dynamic_actions'; - -export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns'; diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts deleted file mode 100644 index f042130158aec..0000000000000 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ /dev/null @@ -1,98 +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 { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, -} from '../../../../src/core/public'; -import { createReactOverlays } from '../../../../src/plugins/kibana_react/public'; -import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { - CONTEXT_MENU_TRIGGER, - PANEL_BADGE_TRIGGER, - EmbeddableSetup, - EmbeddableStart, -} from '../../../../src/plugins/embeddable/public'; -import { - CustomTimeRangeAction, - CUSTOM_TIME_RANGE, - TimeRangeActionContext, -} from './custom_time_range_action'; - -import { - CustomTimeRangeBadge, - CUSTOM_TIME_RANGE_BADGE, - TimeBadgeActionContext, -} from './custom_time_range_badge'; -import { CommonlyUsedRange } from './types'; -import { UiActionsServiceEnhancements } from './services'; - -interface SetupDependencies { - embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. - uiActions: UiActionsSetup; -} - -interface StartDependencies { - embeddable: EmbeddableStart; - uiActions: UiActionsStart; -} - -export interface SetupContract - extends UiActionsSetup, - Pick {} - -export interface StartContract - extends UiActionsStart, - Pick {} - -declare module '../../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [CUSTOM_TIME_RANGE]: TimeRangeActionContext; - [CUSTOM_TIME_RANGE_BADGE]: TimeBadgeActionContext; - } -} - -export class AdvancedUiActionsPublicPlugin - implements Plugin { - private readonly enhancements = new UiActionsServiceEnhancements(); - - constructor(initializerContext: PluginInitializerContext) {} - - public setup(core: CoreSetup, { uiActions }: SetupDependencies): SetupContract { - return { - ...uiActions, - ...this.enhancements, - }; - } - - public start(core: CoreStart, { uiActions }: StartDependencies): StartContract { - const dateFormat = core.uiSettings.get('dateFormat') as string; - const commonlyUsedRanges = core.uiSettings.get('timepicker:quickRanges') as CommonlyUsedRange[]; - const { openModal } = createReactOverlays(core); - const timeRangeAction = new CustomTimeRangeAction({ - openModal, - dateFormat, - commonlyUsedRanges, - }); - uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, timeRangeAction); - - const timeRangeBadge = new CustomTimeRangeBadge({ - openModal, - dateFormat, - commonlyUsedRanges, - }); - uiActions.addTriggerAction(PANEL_BADGE_TRIGGER, timeRangeBadge); - - return { - ...uiActions, - ...this.enhancements, - }; - } - - public stop() {} -} diff --git a/x-pack/plugins/advanced_ui_actions/scripts/storybook.js b/x-pack/plugins/advanced_ui_actions/scripts/storybook.js deleted file mode 100644 index 3da0a3b37bfaf..0000000000000 --- a/x-pack/plugins/advanced_ui_actions/scripts/storybook.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { join } from 'path'; - -// eslint-disable-next-line -require('@kbn/storybook').runStorybookCli({ - name: 'advanced_ui_actions', - storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')], -}); diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 9f20005c2b189..43f3585d0ebb2 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; -import { ILicense } from '../../licensing/public'; +import { ILicense } from '../../licensing/common/types'; import { AGENT_NAME, SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index ae764d171c45c..4bebab8e0c6a8 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -23,6 +23,8 @@ APM_IT_DIR="./tmp/apm-integration-testing" cd ${E2E_DIR} +KIBANA_VERSION=$(node -p "require('../../../package.json').version") + # # Ask user to start Kibana ################################################## @@ -63,7 +65,8 @@ fi # Start apm-integration-testing echo "Starting docker-compose" -${APM_IT_DIR}/scripts/compose.py start master \ +echo "Using stack version: ${KIBANA_VERSION}" +${APM_IT_DIR}/scripts/compose.py start $KIBANA_VERSION \ --no-kibana \ --elasticsearch-port $ELASTICSEARCH_PORT \ --apm-server-port=$APM_SERVER_PORT \ diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index cb8600ed2c214..56c427e67ad4c 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import styled from 'styled-components'; +import { EuiThemeProvider } from '../../../observability/public'; import { CoreStart, AppMountParameters } from '../../../../../src/core/public'; import { ApmPluginSetupDeps } from '../plugin'; import { ApmPluginContext } from '../context/ApmPluginContext'; @@ -18,7 +19,10 @@ import { LocationProvider } from '../context/LocationContext'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + useUiSetting$, +} from '../../../../../src/plugins/kibana_react/public'; import { px, unit, units } from '../style/variables'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; @@ -35,18 +39,22 @@ const MainContainer = styled.div` `; const App = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + return ( - - - - - - {routes.map((route, i) => ( - - ))} - - - + + + + + + + {routes.map((route, i) => ( + + ))} + + + + ); }; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx index bfb9e99b4fc4c..80d5f739bea5a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -82,7 +82,7 @@ export function AlertIntegrations(props: Props) { } ), href: plugin.core.http.basePath.prepend( - '/app/kibana#/management/insightsAndAlerting/triggersActions/alerts' + '/app/management/insightsAndAlerting/triggersActions/alerts' ), icon: 'tableOfContents', }, diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx index bcc31a30b154d..321617ed8496a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx @@ -92,7 +92,7 @@ export class ServiceIntegrations extends React.Component { ), icon: 'watchesApp', href: core.http.basePath.prepend( - '/app/kibana#/management/insightsAndAlerting/watcher' + '/app/management/insightsAndAlerting/watcher' ), target: '_blank', onClick: () => this.closePopover(), diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 5bb678d1c08af..50eb85715969a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -79,6 +79,7 @@ export function AgentConfigurationCreateEdit({ ..._newConfig, settings: existingConfig?.settings || {}, })); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [existingConfig]); // update newConfig when existingConfig has loaded diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index d39ad530c1b4c..1244dd01a3b43 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -109,10 +109,12 @@ export const TransactionDistribution: FunctionComponent = ( bucketIndex, } = props; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const formatYShort = useCallback(getFormatYShort(transactionType), [ transactionType, ]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const formatYLong = useCallback(getFormatYLong(transactionType), [ transactionType, ]); 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 988edb197a230..2507eca9ff663 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -66,6 +66,7 @@ export const TransactionActionMenu: FunctionComponent = ({ { 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] ); diff --git a/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx index 4033d684da981..481e89e09685e 100644 --- a/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx +++ b/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx @@ -11,7 +11,7 @@ import { useApmPluginContext } from '../../hooks/useApmPluginContext'; export function InvalidLicenseNotification() { const { core } = useApmPluginContext(); const manageLicenseURL = core.http.basePath.prepend( - '/app/kibana#/management/stack/license_management' + '/app/management/stack/license_management' ); return ( diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx index 28b836cd2c650..2db4659c83603 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx +++ b/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx @@ -106,6 +106,7 @@ describe('useFetcher', () => { jest.useFakeTimers(); const hook = renderHook( + /* eslint-disable-next-line react-hooks/exhaustive-deps */ ({ callback, args }) => useFetcher(callback, args), { initialProps: { @@ -165,6 +166,7 @@ describe('useFetcher', () => { it('should return the same object reference when data is unchanged between rerenders', async () => { const hook = renderHook( + /* eslint-disable-next-line react-hooks/exhaustive-deps */ ({ callback, args }) => useFetcher(callback, args), { initialProps: { diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 76320efe617ea..0939c51b16605 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -75,7 +75,7 @@ export class ApmPlugin implements Plugin { core.application.register({ id: 'apm', title: 'APM', - order: 8100, + order: 8300, euiIconType: 'apmApp', appRoute: '/app/apm', icon: 'plugins/apm/public/icon.svg', diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index ceed5e6c39716..cb694712d7c97 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -83,13 +83,13 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) **Start server** ``` -node scripts/functional_tests_server --config x-pack/test/api_integration/config.js +node scripts/functional_tests_server --config x-pack/test/api_integration/config.ts ``` **Run tests** ``` -node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs' +node scripts/functional_test_runner --config x-pack/test/api_integration/config.ts --grep='APM specs' ``` APM tests are located in `x-pack/test/api_integration/apis/apm`. diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index c7a17197ca778..892f8f0ddd105 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -19,6 +19,7 @@ import { ESSearchRequest, ESSearchResponse, } from '../../../typings/elasticsearch'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/server'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; import { pickKeys } from '../../../common/utils/pick_keys'; import { APMRequestHandlerContext } from '../../routes/typings'; @@ -95,7 +96,7 @@ async function getParamsForSearchRequest( savedObjectsClient: context.core.savedObjects.client, config: context.config, }), - uiSettings.client.get('search:includeFrozen'), + uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); // Get indices for legacy data filter (only those which apply) diff --git a/x-pack/plugins/apm/typings/numeral.d.ts b/x-pack/plugins/apm/typings/numeral.d.ts deleted file mode 100644 index 2616639cdeff0..0000000000000 --- a/x-pack/plugins/apm/typings/numeral.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -interface Numeral { - (value?: unknown): Numeral; - format: (pattern: string) => string; - unformat: (pattern: string) => number; - language: (key?: any, values?: any) => Numeral; - set: (value?: any) => Numeral; -} - -// eslint-disable-next-line no-var -declare var numeral: Numeral; - -declare module '@elastic/numeral' { - export = numeral; -} diff --git a/x-pack/plugins/beats_management/public/application.tsx b/x-pack/plugins/beats_management/public/application.tsx index 6711e93895b62..0b8b0ad3dafc9 100644 --- a/x-pack/plugins/beats_management/public/application.tsx +++ b/x-pack/plugins/beats_management/public/application.tsx @@ -8,7 +8,7 @@ import * as euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import React from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; import { Provider as UnstatedProvider, Subscribe } from 'unstated'; import { Background } from './components/layouts/background'; @@ -19,19 +19,13 @@ import { TagsContainer } from './containers/tags'; import { FrontendLibs } from './lib/types'; import { AppRouter } from './router'; import { services } from './kbn_services'; -import { - ManagementAppMountParams, - ManagementSectionId, -} from '../../../../src/plugins/management/public'; +import { ManagementAppMountParams } from '../../../../src/plugins/management/public'; -export const renderApp = ( - { basePath, element, setBreadcrumbs }: ManagementAppMountParams, - libs: FrontendLibs -) => { +export const renderApp = ({ element, history }: ManagementAppMountParams, libs: FrontendLibs) => { ReactDOM.render( - + @@ -48,7 +42,7 @@ export const renderApp = ( - +
, element diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index 7256657903aab..14409ae166a84 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -7,6 +7,7 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; import { SetupInitializer } from '../../plugin'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; export const metricElementInitializer: SetupInitializer = (core, setup) => { return () => ({ @@ -23,7 +24,7 @@ export const metricElementInitializer: SetupInitializer = (core, | metric "Countries" metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${core.uiSettings.get('format:number:defaultPattern')}" + metricFormat="${core.uiSettings.get(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" | render`, }); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 2dd116d5ada08..ad368a912cd8c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -11,12 +11,10 @@ import { StartDeps } from '../../plugin'; import { IEmbeddable, EmbeddableFactory, - EmbeddablePanel, EmbeddableFactoryNotFoundError, } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableExpression } from '../../expression_types/embeddable'; import { RendererStrings } from '../../../i18n'; -import { getSavedObjectFinder } from '../../../../../../src/plugins/saved_objects/public'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; import { EmbeddableInput } from '../../expression_types'; import { RendererHandlers } from '../../../types'; @@ -38,17 +36,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { style={{ width: domNode.offsetWidth, height: domNode.offsetHeight, cursor: 'auto' }} > - +
); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx index ef5bfb70d4b3d..25278adcf4529 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx @@ -7,6 +7,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { toExpression } from '@kbn/interpreter/common'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; import { RendererStrings } from '../../../i18n'; import { TimeFilter } from './components'; @@ -20,7 +21,7 @@ const { timeFilter: strings } = RendererStrings; export const timeFilterFactory: StartInitializer> = (core, plugins) => { const { uiSettings } = core; - const customQuickRanges = (uiSettings.get('timepicker:quickRanges') || []).map( + const customQuickRanges = (uiSettings.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES) || []).map( ({ from, to, display }: { from: string; to: string; display: string }) => ({ start: from, end: to, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts index 4025d4deaf997..5a3e3904f4f23 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -11,6 +11,7 @@ import { templateFromReactComponent } from '../../../../public/lib/template_from import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; import { SetupInitializer } from '../../../plugin'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; const { NumberFormat: strings } = ArgumentStrings; @@ -19,11 +20,11 @@ export const numberFormatInitializer: SetupInitializer { const formatMap = { - NUMBER: core.uiSettings.get('format:number:defaultPattern'), - PERCENT: core.uiSettings.get('format:percent:defaultPattern'), - CURRENCY: core.uiSettings.get('format:currency:defaultPattern'), + NUMBER: core.uiSettings.get(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN), + PERCENT: core.uiSettings.get(UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN), + CURRENCY: core.uiSettings.get(UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN), DURATION: '00:00:00', - BYTES: core.uiSettings.get('format:bytes:defaultPattern'), + BYTES: core.uiSettings.get(UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN), }; const numberFormats = [ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts index 93912b7b0517f..11bee46088576 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts @@ -7,6 +7,7 @@ import { openSans } from '../../../common/lib/fonts'; import { ViewStrings } from '../../../i18n'; import { SetupInitializer } from '../../plugin'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; const { Metric: strings } = ViewStrings; @@ -22,7 +23,7 @@ export const metricInitializer: SetupInitializer = (core, plugin) => { displayName: strings.getMetricFormatDisplayName(), help: strings.getMetricFormatHelp(), argType: 'numberFormat', - default: `"${core.uiSettings.get('format:number:defaultPattern')}"`, + default: `"${core.uiSettings.get(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}"`, }, { name: '_', diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.examples.tsx b/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.examples.tsx index 51caf1db196bc..d010f4a554b87 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.examples.tsx +++ b/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.examples.tsx @@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -import { monaco } from '@kbn/ui-shared-deps/monaco'; +import { monaco } from '@kbn/monaco'; import { ExpressionInput } from '../expression_input'; import { language, LANGUAGE_ID } from '../../../lib/monaco_language_def'; diff --git a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx index 99e12b14104be..5ada495208fba 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx +++ b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx @@ -8,7 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow } from '@elastic/eui'; import { debounce } from 'lodash'; -import { monaco } from '@kbn/ui-shared-deps/monaco'; +import { monaco } from '@kbn/monaco'; import { ExpressionFunction } from '../../../types'; import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public'; import { diff --git a/x-pack/plugins/canvas/public/lib/monaco_language_def.ts b/x-pack/plugins/canvas/public/lib/monaco_language_def.ts index 7bd9ea7b6ef9a..5b88658ddcd63 100644 --- a/x-pack/plugins/canvas/public/lib/monaco_language_def.ts +++ b/x-pack/plugins/canvas/public/lib/monaco_language_def.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { monaco } from '@kbn/ui-shared-deps/monaco'; +import { monaco } from '@kbn/monaco'; import { ExpressionFunction } from '../../types'; export const LANGUAGE_ID = 'canvas-expression'; diff --git a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts index 96884cf4bead8..0574cb9b2dd6d 100644 --- a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts +++ b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts @@ -24,8 +24,7 @@ export const APPS = { }; export const MANAGEMENT_ID = 'cross_cluster_replication'; -export const BASE_PATH = `/management/data/${MANAGEMENT_ID}`; -export const BASE_PATH_REMOTE_CLUSTERS = '/management/data/remote_clusters'; +export const BASE_PATH_REMOTE_CLUSTERS = 'data/remote_clusters'; export const API_BASE_PATH = '/api/cross_cluster_replication'; export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters'; export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management'; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js index e240b5b304c33..a4edee7268a23 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js @@ -12,7 +12,11 @@ import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, memoryRouter: { - onRouter: (router) => (routing.reactRouter = router), + onRouter: (router) => + (routing.reactRouter = { + ...router, + getUrlForApp: () => '', + }), }, }; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js index c32f6a54114c7..ea372d534d817 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js @@ -14,7 +14,11 @@ import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants'; const testBedConfig = { store: ccrStore, memoryRouter: { - onRouter: (router) => (routing.reactRouter = router), + onRouter: (router) => + (routing.reactRouter = { + ...router, + getUrlForApp: () => '', + }), // The auto-follow pattern id to fetch is read from the router ":id" param // so we first set it in our initial entries initialEntries: [`/${AUTO_FOLLOW_PATTERN_EDIT_NAME}`], diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js index 87cf6a13ba011..550e178a1c733 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js @@ -12,7 +12,18 @@ import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, memoryRouter: { - onRouter: (router) => (routing.reactRouter = router), + onRouter: (router) => + (routing.reactRouter = { + ...router, + history: { + ...router.history, + parentHistory: { + createHref: () => '', + push: () => {}, + }, + }, + getUrlForApp: () => '', + }), }, }; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js index 9778b595111a2..31f3844f57fb5 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js @@ -12,7 +12,11 @@ import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, memoryRouter: { - onRouter: (router) => (routing.reactRouter = router), + onRouter: (router) => + (routing.reactRouter = { + ...router, + getUrlForApp: () => '', + }), }, }; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js index 993cde86ada22..8fc2811e58cdb 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js @@ -14,7 +14,11 @@ import { FOLLOWER_INDEX_EDIT_NAME } from './constants'; const testBedConfig = { store: ccrStore, memoryRouter: { - onRouter: (router) => (routing.reactRouter = router), + onRouter: (router) => + (routing.reactRouter = { + ...router, + getUrlForApp: () => '', + }), // The follower index id to fetch is read from the router ":id" param // so we first set it in our initial entries initialEntries: [`/${FOLLOWER_INDEX_EDIT_NAME}`], diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js index 16a4fa9cd3f3e..65be10b9d272e 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js @@ -12,7 +12,17 @@ import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, memoryRouter: { - onRouter: (router) => (routing.reactRouter = router), + onRouter: (router) => + (routing.reactRouter = { + history: { + ...router.history, + parentHistory: { + createHref: () => '', + push: () => {}, + }, + }, + getUrlForApp: () => '', + }), }, }; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js index 26a0d86cdcbca..e51c444a6b370 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js @@ -5,7 +5,6 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { BASE_PATH } from '../../../../common/constants'; import { CrossClusterReplicationHome } from '../../../app/sections/home/home'; import { ccrStore } from '../../../app/store'; import { routing } from '../../../app/services/routing'; @@ -13,8 +12,8 @@ import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, memoryRouter: { - initialEntries: [`${BASE_PATH}/follower_indices`], - componentRoutePath: `${BASE_PATH}/:section`, + initialEntries: [`/follower_indices`], + componentRoutePath: `/:section`, onRouter: (router) => (routing.reactRouter = router), }, }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx index ec349ccd6f2c7..288da20c353d2 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx +++ b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx @@ -5,8 +5,8 @@ */ import React, { Component, Fragment } from 'react'; -import { Route, Switch, Redirect, withRouter, RouteComponentProps } from 'react-router-dom'; -import { History } from 'history'; +import { Route, Switch, Router, Redirect } from 'react-router-dom'; +import { ScopedHistory, ApplicationStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -20,7 +20,6 @@ import { EuiTitle, } from '@elastic/eui'; -import { BASE_PATH } from '../../common/constants'; import { getFatalErrors } from './services/notifications'; import { SectionError } from './components'; import { routing } from './services/routing'; @@ -37,8 +36,8 @@ import { } from './sections'; interface AppProps { - history: History; - location: any; + history: ScopedHistory; + getUrlForApp: ApplicationStart['getUrlForApp']; } interface AppState { @@ -48,7 +47,7 @@ interface AppState { missingClusterPrivileges: any[]; } -class AppComponent extends Component { +class AppComponent extends Component { constructor(props: any) { super(props); this.registerRouter(); @@ -99,12 +98,13 @@ class AppComponent extends Component { } registerRouter() { - const { history, location } = this.props; + const { history, getUrlForApp } = this.props; routing.reactRouter = { history, route: { - location, + location: history.location, }, + getUrlForApp, }; } @@ -189,30 +189,18 @@ class AppComponent extends Component { } return ( -
+ - - - - - - + + + + + + -
+ ); } } -export const App = withRouter(AppComponent); +export const App = AppComponent; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx index 5474708f313c8..1d8f8bacc8c84 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx @@ -99,7 +99,7 @@ const AutoFollowPatternActionMenuUI: FunctionComponent = ({ }), icon: , onClick: () => { - window.location.hash = routing.getAutoFollowPatternPath(patterns[0].name); + routing.navigate(routing.getAutoFollowPatternPath(patterns[0].name)); }, } : null, diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js index 6e4c019dab85c..101e3df6bf710 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js @@ -10,7 +10,7 @@ import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiLink, EuiOverlayMask } from '@elastic/eui'; - +import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public'; import { routing } from '../services/routing'; import { resumeFollowerIndex } from '../store/actions'; import { arrify } from '../../../common/services/utils'; @@ -97,7 +97,13 @@ class FollowerIndexResumeProviderUi extends PureComponent { custom advanced settings, {editLink}." values={{ editLink: ( - + ( @@ -152,10 +151,9 @@ export class RemoteClustersFormField extends PureComponent { {' '} {/* Break out of EuiFormRow's flexbox layout */} {this.errorMessages.noClusterFound()}

{description}

{this.errorMessages.remoteClusterDoesNotExist(name)}

{ +const renderApp = ( + element: Element, + I18nContext: I18nStart['Context'], + history: ScopedHistory, + getUrlForApp: ApplicationStart['getUrlForApp'] +): UnmountCallback => { render( - - - + , element @@ -36,17 +38,21 @@ export async function mountApp({ I18nContext, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION, + history, + getUrlForApp, }: { element: Element; setBreadcrumbs: SetBreadcrumbs; I18nContext: I18nStart['Context']; ELASTIC_WEBSITE_URL: string; DOC_LINK_VERSION: string; + history: ScopedHistory; + getUrlForApp: ApplicationStart['getUrlForApp']; }): Promise { // Import and initialize additional services here instead of in plugin.ts to reduce the size of the // initial bundle as much as possible. initBreadcrumbs(setBreadcrumbs); initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`); - return renderApp(element, I18nContext); + return renderApp(element, I18nContext, history, getUrlForApp); } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js index 60a6cc79376e5..76fdf6e2fd766 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js @@ -27,7 +27,7 @@ export class AutoFollowPatternAdd extends PureComponent { }; componentDidMount() { - setBreadcrumbs([listBreadcrumb, addBreadcrumb]); + setBreadcrumbs([listBreadcrumb('/auto_follow_patterns'), addBreadcrumb]); } componentWillUnmount() { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js index 387d7817a0357..d89e3adb531ab 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui'; import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; -import { routing } from '../../services/routing'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { AutoFollowPatternForm, AutoFollowPatternPageTitle, @@ -54,7 +54,7 @@ export class AutoFollowPatternEdit extends PureComponent { selectAutoFollowPattern(decodedId); - setBreadcrumbs([listBreadcrumb, editBreadcrumb]); + setBreadcrumbs([listBreadcrumb('/auto_follow_patterns'), editBreadcrumb]); } componentDidUpdate(prevProps, prevState) { @@ -108,7 +108,7 @@ export class AutoFollowPatternEdit extends PureComponent { @@ -122,7 +122,7 @@ export class AutoFollowPatternList extends PureComponent { {isAuthorized && ( (window.location.hash = routing.getAutoFollowPatternPath(name))} + onClick={() => routing.navigate(routing.getAutoFollowPatternPath(name))} data-test-subj="contextMenuEditButton" > diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js index 3f2ed82420ff1..6b2ee01bff8df 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js @@ -31,6 +31,7 @@ import { EuiTitle, } from '@elastic/eui'; +import { routing } from '../../../../../services/routing'; import { AutoFollowPatternIndicesPreview, AutoFollowPatternActionMenu, @@ -296,7 +297,12 @@ export class DetailPanel extends Component { - + { - const uri = routing.getFollowerIndexPath(id, '/edit', false); + const uri = routing.getFollowerIndexPath(id); routing.navigate(uri); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js index 4436d76643e6c..a133c10b148aa 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js @@ -32,6 +32,7 @@ import { import 'brace/theme/textmate'; import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public'; +import { routing } from '../../../../../services/routing'; import { API_STATUS } from '../../../../../constants'; import { ContextMenu } from '../context_menu'; @@ -452,7 +453,12 @@ export class DetailPanel extends Component { - + { - const uri = routing.getFollowerIndexPath(id, '/edit', false); + const uri = routing.getFollowerIndexPath(id); routing.navigate(uri); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index 7b843d08cefd3..4d4cbbf6825ec 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { routing } from '../../../services/routing'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { extractQueryParams } from '../../../services/query_params'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; @@ -94,7 +94,7 @@ export class FollowerIndicesList extends PureComponent { } renderHeader() { - const { isAuthorized } = this.props; + const { isAuthorized, history } = this.props; return ( @@ -113,7 +113,7 @@ export class FollowerIndicesList extends PureComponent { {isAuthorized && ( { + setBreadcrumbs([listBreadcrumb(`/${section}`)]); routing.navigate(`/${section}`); }; @@ -94,12 +94,8 @@ export class CrossClusterReplicationHome extends PureComponent { - - + +
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts index 84ac9356462ad..c3ca893e6182b 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts @@ -9,8 +9,6 @@ import { ChromeBreadcrumb } from 'src/core/public'; import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; -import { BASE_PATH } from '../../../common/constants'; - export type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; let setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; @@ -19,11 +17,13 @@ export const init = (_setBreadcrumbs: SetBreadcrumbs): void => { setBreadcrumbs = _setBreadcrumbs; }; -export const listBreadcrumb = { - text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', { - defaultMessage: 'Cross-Cluster Replication', - }), - href: `#${BASE_PATH}`, +export const listBreadcrumb = (section?: string) => { + return { + text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', { + defaultMessage: 'Cross-Cluster Replication', + }), + href: section || '/', + }; }; export const addBreadcrumb = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js index 1a488cc951c49..59210272534b9 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js @@ -8,14 +8,8 @@ * This file based on guidance from https://github.com/elastic/eui/blob/master/wiki/react-router.md */ -import { createLocation } from 'history'; import { stringify } from 'query-string'; -import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; - -const isModifiedEvent = (event) => - !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); - -const isLeftClickEvent = (event) => event.button === 0; +import { BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; const queryParamsFromObject = (params, encodeParams = false) => { if (!params) { @@ -26,67 +20,31 @@ const queryParamsFromObject = (params, encodeParams = false) => { return `?${paramsStr}`; }; -const appToBasePathMap = { - [APPS.CCR_APP]: BASE_PATH, - [APPS.REMOTE_CLUSTER_APP]: BASE_PATH_REMOTE_CLUSTERS, -}; - class Routing { _reactRouter = null; - /** - * The logic for generating hrefs and onClick handlers from the `to` prop is largely borrowed from - * https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js. - * - * @param {*} to URL to navigate to - */ - getRouterLinkProps(to, base = BASE_PATH, params = {}, encodeParams = false) { + getHrefToRemoteClusters(route = '/', params, encodeParams = false) { const search = queryParamsFromObject(params, encodeParams) || ''; - const location = - typeof to === 'string' - ? createLocation(base + to + search, null, null, this._reactRouter.history.location) - : to; - const href = this._reactRouter.history.createHref(location); - - const onClick = (event) => { - if (event.defaultPrevented) { - return; - } - - // If target prop is set (e.g. to "_blank"), let browser handle link. - if (event.target.getAttribute('target')) { - return; - } - - if (isModifiedEvent(event) || !isLeftClickEvent(event)) { - return; - } - - // Prevent regular link behavior, which causes a browser refresh. - event.preventDefault(); - this._reactRouter.history.push(location); - }; - - return { href, onClick }; + return this._reactRouter.getUrlForApp('management', { + path: `${BASE_PATH_REMOTE_CLUSTERS}${route}${search}`, + }); } - navigate(route = '/home', app = APPS.CCR_APP, params, encodeParams = false) { + navigate(route = '/home', params, encodeParams = false) { const search = queryParamsFromObject(params, encodeParams); this._reactRouter.history.push({ - pathname: encodeURI(appToBasePathMap[app] + route), + pathname: encodeURI(route), search, }); } getAutoFollowPatternPath = (name, section = '/edit') => { - return encodeURI(`#${BASE_PATH}/auto_follow_patterns${section}/${encodeURIComponent(name)}`); + return encodeURI(`/auto_follow_patterns${section}/${encodeURIComponent(name)}`); }; - getFollowerIndexPath = (name, section = '/edit', withBase = true) => { - return withBase - ? encodeURI(`#${BASE_PATH}/follower_indices${section}/${encodeURIComponent(name)}`) - : encodeURI(`/follower_indices${section}/${encodeURIComponent(name)}`); + getFollowerIndexPath = (name, section = '/edit') => { + return encodeURI(`/follower_indices${section}/${encodeURIComponent(name)}`); }; get reactRouter() { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js index ea6801b55458d..6503333924951 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js @@ -76,7 +76,7 @@ export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) ); getToasts().addSuccess(successMessage); - routing.navigate(`/auto_follow_patterns`, undefined, { + routing.navigate(`/auto_follow_patterns`, { pattern: encodeURIComponent(id), }); }, diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js index 61d0ed1d51c72..1af5a95a29b98 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js @@ -77,7 +77,7 @@ export const saveFollowerIndex = (name, followerIndex, isUpdating = false) => ); getToasts().addSuccess(successMessage); - routing.navigate(`/follower_indices`, undefined, { + routing.navigate(`/follower_indices`, { name: encodeURIComponent(name), }); }, diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts index e748822ab8ae7..8bf0d519e685d 100644 --- a/x-pack/plugins/cross_cluster_replication/public/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -41,13 +41,14 @@ export class CrossClusterReplicationPlugin implements Plugin { id: MANAGEMENT_ID, title: PLUGIN.TITLE, order: 6, - mount: async ({ element, setBreadcrumbs }) => { + mount: async ({ element, setBreadcrumbs, history }) => { const { mountApp } = await import('./app'); const [coreStart] = await getStartServices(); const { i18n: { Context: I18nContext }, docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + application: { getUrlForApp }, } = coreStart; return mountApp({ @@ -56,12 +57,14 @@ export class CrossClusterReplicationPlugin implements Plugin { I18nContext, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION, + history, + getUrlForApp, }); }, }); - ccrApp.disable(); - + // NOTE: We enable the plugin by default instead of disabling it by default because this + // creates a race condition that causes functional tests to fail on CI (see #66781). licensing.license$ .pipe(first()) .toPromise() @@ -76,8 +79,6 @@ export class CrossClusterReplicationPlugin implements Plugin { const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; if (isLicenseOk && isCcrUiEnabled) { - ccrApp.enable(); - if (indexManagement) { const propertyPath = 'isFollowerIndex'; @@ -94,6 +95,8 @@ export class CrossClusterReplicationPlugin implements Plugin { indexManagement.extensionsService.addBadge(followerBadgeExtension); } + } else { + ccrApp.disable(); } }); } diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index f416ca97f7110..37211ea537179 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["data", "advancedUiActions", "drilldowns", "embeddable", "dashboard", "share"], + "requiredPlugins": ["data", "uiActionsEnhanced", "drilldowns", "embeddable", "dashboard", "share"], "configPath": ["xpack", "dashboardEnhanced"] } diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts index c258a4148f84a..413f5a7afe356 100644 --- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -9,19 +9,19 @@ import { SharePluginStart, SharePluginSetup } from '../../../../src/plugins/shar import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { DashboardDrilldownsService } from './services'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../advanced_ui_actions/public'; +import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../ui_actions_enhanced/public'; import { DrilldownsSetup, DrilldownsStart } from '../../drilldowns/public'; import { DashboardStart } from '../../../../src/plugins/dashboard/public'; export interface SetupDependencies { - advancedUiActions: AdvancedUiActionsSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; drilldowns: DrilldownsSetup; embeddable: EmbeddableSetup; share: SharePluginSetup; } export interface StartDependencies { - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; data: DataPublicPluginStart; drilldowns: DrilldownsStart; embeddable: EmbeddableStart; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx index 555acf1fca5ff..309e6cbf53a3d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx @@ -8,7 +8,7 @@ import { FlyoutEditDrilldownAction, FlyoutEditDrilldownParams } from './flyout_e import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { drilldownsPluginMock } from '../../../../../../drilldowns/public/mocks'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { uiActionsEnhancedPluginMock } from '../../../../../../advanced_ui_actions/public/mocks'; +import { uiActionsEnhancedPluginMock } from '../../../../../../ui_actions_enhanced/public/mocks'; import { EnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx index ec3a78e97eae4..9a4ecb2d4bfb0 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, cleanup, act } from '@testing-library/react/pure'; import { MenuItem } from './menu_item'; import { createStateContainer } from '../../../../../../../../src/plugins/kibana_utils/public'; -import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../../../../../advanced_ui_actions/public'; +import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../../../../../ui_actions_enhanced/public'; import { EnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; import '@testing-library/jest-dom'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts index cccacf701a9ad..e831f87baa11c 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts @@ -10,9 +10,9 @@ import { UiActionsEnhancedMemoryActionStorage as MemoryActionStorage, UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsStart, -} from '../../../../../advanced_ui_actions/public'; +} from '../../../../../ui_actions_enhanced/public'; import { TriggerContextMapping } from '../../../../../../../src/plugins/ui_actions/public'; -import { uiActionsEnhancedPluginMock } from '../../../../../advanced_ui_actions/public/mocks'; +import { uiActionsEnhancedPluginMock } from '../../../../../ui_actions_enhanced/public/mocks'; export class MockEmbeddable extends Embeddable { public rootType = 'dashboard'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index f5926cd6961c2..4325e3309b898 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -41,7 +41,7 @@ export class DashboardDrilldownsService { setupDrilldowns( core: CoreSetup, - { advancedUiActions: uiActions }: SetupDependencies + { uiActionsEnhanced: uiActions }: SetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); const getDashboardUrlGenerator = () => { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index c94d19d28e6da..6ce7dccd3a3ec 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -101,13 +101,13 @@ describe('.execute() & getHref', () => { }, }, plugins: { - advancedUiActions: {}, + uiActionsEnhanced: {}, data: { actions: dataPluginActions, }, }, self: {}, - })) as unknown) as StartServicesGetter>, + })) as unknown) as StartServicesGetter>, getDashboardUrlGenerator: () => new UrlGeneratorsService().setup(coreMock.createSetup()).registerUrlGenerator( createDashboardUrlGenerator(() => diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 7ff84a75dd52c..26a69132cffb1 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -10,7 +10,7 @@ import { DashboardUrlGenerator } from '../../../../../../../src/plugins/dashboar import { ActionContext, Config } from './types'; import { CollectConfigContainer } from './components'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; @@ -22,7 +22,7 @@ import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_uti import { StartDependencies } from '../../../plugin'; export interface Params { - start: StartServicesGetter>; + start: StartServicesGetter>; getDashboardUrlGenerator: () => DashboardUrlGenerator; } diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts index c493e8ce86781..3a511c7b5a176 100644 --- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts @@ -11,6 +11,7 @@ import { ISearchContext, ISearch, getEsPreference, + UI_SETTINGS, } from '../../../../../src/plugins/data/public'; import { IEnhancedEsSearchRequest, EnhancedSearchParams } from '../../common'; import { ASYNC_SEARCH_STRATEGY } from './async_search_strategy'; @@ -27,7 +28,7 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider { const params: EnhancedSearchParams = { - ignoreThrottled: !context.core.uiSettings.get('search:includeFrozen'), + ignoreThrottled: !context.core.uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), preference: getEsPreference(context.core.uiSettings), ...request.params, }; diff --git a/x-pack/plugins/drilldowns/kibana.json b/x-pack/plugins/drilldowns/kibana.json index 678c054aa322c..1614f94b488fd 100644 --- a/x-pack/plugins/drilldowns/kibana.json +++ b/x-pack/plugins/drilldowns/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["uiActions", "embeddable", "advancedUiActions"], + "requiredPlugins": ["uiActions", "embeddable", "uiActionsEnhanced"], "configPath": ["xpack", "drilldowns"] } diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx index a186feec33924..5fde4fc79e433 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx @@ -12,13 +12,13 @@ import { dashboardFactory, urlFactory, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; +} from '../../../../ui_actions_enhanced/public/components/action_wizard/test_data'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { StubBrowserStorage } from '../../../../../../src/test_utils/public/stub_browser_storage'; import { mockDynamicActionManager } from './test_data'; const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ - advancedUiActions: { + uiActionsEnhanced: { getActionFactories() { return [dashboardFactory, urlFactory]; }, diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index 0f7f0cb22760b..32cbec795d092 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -11,7 +11,7 @@ import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldow import { dashboardFactory, urlFactory, -} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; +} from '../../../../ui_actions_enhanced/public/components/action_wizard/test_data'; import { StubBrowserStorage } from '../../../../../../src/test_utils/public/stub_browser_storage'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { mockDynamicActionManager } from './test_data'; @@ -24,7 +24,7 @@ import { toastDrilldownsCRUDError } from './i18n'; const storage = new Storage(new StubBrowserStorage()); const notifications = coreMock.createStart().notifications; const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ - advancedUiActions: { + uiActionsEnhanced: { getActionFactories() { return [dashboardFactory, urlFactory]; }, diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 3c9d2d2a86fb1..45cf7365ebd91 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -7,12 +7,12 @@ import React, { useEffect, useState } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; import { - AdvancedUiActionsActionFactory as ActionFactory, + UiActionsEnhancedActionFactory as ActionFactory, AdvancedUiActionsStart, UiActionsEnhancedDynamicActionManager as DynamicActionManager, UiActionsEnhancedSerializedAction, UiActionsEnhancedSerializedEvent, -} from '../../../../advanced_ui_actions/public'; +} from '../../../../ui_actions_enhanced/public'; import { NotificationsStart } from '../../../../../../src/core/public'; import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard'; import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns'; @@ -48,17 +48,17 @@ enum Routes { } export function createFlyoutManageDrilldowns({ - advancedUiActions, + uiActionsEnhanced, storage, notifications, }: { - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; storage: IStorageWrapper; notifications: NotificationsStart; }) { // fine to assume this is static, // because all action factories should be registered in setup phase - const allActionFactories = advancedUiActions.getActionFactories(); + const allActionFactories = uiActionsEnhanced.getActionFactories(); const allActionFactoriesById = allActionFactories.reduce((acc, next) => { acc[next.id] = next; return acc; diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts index c9cb0b0eb1cb3..d585fa0692e8c 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts @@ -9,7 +9,7 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, UiActionsEnhancedDynamicActionManagerState as DynamicActionManagerState, UiActionsEnhancedSerializedAction, -} from '../../../../advanced_ui_actions/public'; +} from '../../../../ui_actions_enhanced/public'; import { TriggerContextMapping } from '../../../../../../src/plugins/ui_actions/public'; import { createStateContainer } from '../../../../../../src/plugins/kibana_utils/common'; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx index add8b748afee9..be048bf920602 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx @@ -14,8 +14,8 @@ import { dashboardFactory, urlFactory, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; -import { AdvancedUiActionsActionFactory as ActionFactory } from '../../../../advanced_ui_actions/public/'; +} from '../../../../ui_actions_enhanced/public/components/action_wizard/test_data'; +import { UiActionsEnhancedActionFactory as ActionFactory } from '../../../../ui_actions_enhanced/public/'; storiesOf('components/FlyoutDrilldownWizard', module) .add('default', () => { diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index 84c1a04a71d15..87f886817517f 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -16,7 +16,7 @@ import { txtEditDrilldownTitle, } from './i18n'; import { DrilldownHelloBar } from '../drilldown_hello_bar'; -import { AdvancedUiActionsActionFactory as ActionFactory } from '../../../../advanced_ui_actions/public'; +import { UiActionsEnhancedActionFactory as ActionFactory } from '../../../../ui_actions_enhanced/public'; export interface DrilldownWizardConfig { name: string; diff --git a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx index 38168377b02bd..1813851d728db 100644 --- a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n'; import { - AdvancedUiActionsActionFactory as ActionFactory, + UiActionsEnhancedActionFactory as ActionFactory, ActionWizard, -} from '../../../../advanced_ui_actions/public'; +} from '../../../../ui_actions_enhanced/public'; const noopFn = () => {}; diff --git a/x-pack/plugins/drilldowns/public/plugin.ts b/x-pack/plugins/drilldowns/public/plugin.ts index 0108e04df9c99..32176241c102f 100644 --- a/x-pack/plugins/drilldowns/public/plugin.ts +++ b/x-pack/plugins/drilldowns/public/plugin.ts @@ -6,18 +6,18 @@ import { CoreStart, CoreSetup, Plugin } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../advanced_ui_actions/public'; +import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../ui_actions_enhanced/public'; import { createFlyoutManageDrilldowns } from './components/connected_flyout_manage_drilldowns'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; export interface SetupDependencies { uiActions: UiActionsSetup; - advancedUiActions: AdvancedUiActionsSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface StartDependencies { uiActions: UiActionsStart; - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; } // eslint-disable-next-line @@ -36,7 +36,7 @@ export class DrilldownsPlugin public start(core: CoreStart, plugins: StartDependencies): StartContract { return { FlyoutManageDrilldowns: createFlyoutManageDrilldowns({ - advancedUiActions: plugins.advancedUiActions, + uiActionsEnhanced: plugins.uiActionsEnhanced, storage: new Storage(localStorage), notifications: core.notifications, }), diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 780a1d5d89870..5663671de7bd9 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["embeddable", "advancedUiActions"] + "requiredPlugins": ["embeddable", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts index f8b3a9dfb92d0..5c5d98d75295d 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts @@ -9,7 +9,7 @@ import { EmbeddableActionStorage, EmbeddableWithDynamicActionsInput, } from './embeddable_action_storage'; -import { UiActionsEnhancedSerializedEvent } from '../../../advanced_ui_actions/public'; +import { UiActionsEnhancedSerializedEvent } from '../../../ui_actions_enhanced/public'; import { of } from '../../../../../src/plugins/kibana_utils/public'; class TestEmbeddable extends Embeddable { diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts index e93674ba650a7..fdc42585a80ce 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts @@ -7,7 +7,7 @@ import { UiActionsEnhancedAbstractActionStorage as AbstractActionStorage, UiActionsEnhancedSerializedEvent as SerializedEvent, -} from '../../../advanced_ui_actions/public'; +} from '../../../ui_actions_enhanced/public'; import { EmbeddableInput, EmbeddableOutput, diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index d26acb4459a71..e6413ac03aeae 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -27,7 +27,7 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, -} from '../../advanced_ui_actions/public'; +} from '../../ui_actions_enhanced/public'; import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; declare module '../../../../src/plugins/ui_actions/public' { @@ -38,12 +38,12 @@ declare module '../../../../src/plugins/ui_actions/public' { export interface SetupDependencies { embeddable: EmbeddableSetup; - advancedUiActions: AdvancedUiActionsSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface StartDependencies { embeddable: EmbeddableStart; - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; } // eslint-disable-next-line @@ -56,20 +56,20 @@ export class EmbeddableEnhancedPlugin implements Plugin { constructor(protected readonly context: PluginInitializerContext) {} - private uiActions?: StartDependencies['advancedUiActions']; + private uiActions?: StartDependencies['uiActionsEnhanced']; public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); const panelNotificationAction = new PanelNotificationsAction(); - plugins.advancedUiActions.registerAction(panelNotificationAction); - plugins.advancedUiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); + plugins.uiActionsEnhanced.registerAction(panelNotificationAction); + plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); return {}; } public start(core: CoreStart, plugins: StartDependencies): StartContract { - this.uiActions = plugins.advancedUiActions; + this.uiActions = plugins.uiActionsEnhanced; return {}; } diff --git a/x-pack/plugins/embeddable_enhanced/public/types.ts b/x-pack/plugins/embeddable_enhanced/public/types.ts index 924605be332b2..4f5c316f2fc17 100644 --- a/x-pack/plugins/embeddable_enhanced/public/types.ts +++ b/x-pack/plugins/embeddable_enhanced/public/types.ts @@ -5,7 +5,7 @@ */ import { IEmbeddable } from '../../../../src/plugins/embeddable/public'; -import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../advanced_ui_actions/public'; +import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../ui_actions_enhanced/public'; export type EnhancedEmbeddable = E & { enhancements: { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 0144e573fc146..ada86adf84cfd 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -51,7 +51,7 @@ describe('doesIlmPolicyExist', () => { await clusterClientAdapter.doesIlmPolicyExist('foo'); expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { method: 'GET', - path: '_ilm/policy/foo', + path: '/_ilm/policy/foo', }); }); @@ -78,7 +78,7 @@ describe('createIlmPolicy', () => { await clusterClientAdapter.createIlmPolicy('foo', { args: true }); expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { method: 'PUT', - path: '_ilm/policy/foo', + path: '/_ilm/policy/foo', body: { args: true }, }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 7fd239ca49369..a036bfb74e408 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -41,7 +41,7 @@ export class ClusterClientAdapter { public async doesIlmPolicyExist(policyName: string): Promise { const request = { method: 'GET', - path: `_ilm/policy/${policyName}`, + path: `/_ilm/policy/${policyName}`, }; try { await this.callEs('transport.request', request); @@ -55,7 +55,7 @@ export class ClusterClientAdapter { public async createIlmPolicy(policyName: string, policy: unknown): Promise { const request = { method: 'PUT', - path: `_ilm/policy/${policyName}`, + path: `/_ilm/policy/${policyName}`, body: policy, }; try { diff --git a/x-pack/plugins/global_search/README.md b/x-pack/plugins/global_search/README.md new file mode 100644 index 0000000000000..d47e0bd696fd8 --- /dev/null +++ b/x-pack/plugins/global_search/README.md @@ -0,0 +1,49 @@ +# Kibana GlobalSearch plugin + +The GlobalSearch plugin provides an easy way to search for various objects, such as applications +or dashboards from the Kibana instance, from both server and client-side plugins + +## Consuming the globalSearch API + +```ts +startDeps.globalSearch.find('some term').subscribe({ + next: ({ results }) => { + addNewResultsToList(results); + }, + error: () => {}, + complete: () => { + showAsyncSearchIndicator(false); + } +}); +``` + +## Registering custom result providers + +The GlobalSearch API allows to extend provided results by registering your own provider. + +```ts +setupDeps.globalSearch.registerResultProvider({ + id: 'my_provider', + find: (term, options, context) => { + const resultPromise = myService.search(term, context.core.savedObjects.client); + return from(resultPromise).pipe(takeUntil(options.aborted$); + }, +}); +``` + +## Known limitations + +### Client-side registered providers + +Results from providers registered from the client-side `registerResultProvider` API will +not be available when performing a search from the server-side. For this reason, prefer +registering providers using the server-side API when possible. + +Refer to the [RFC](rfcs/text/0011_global_search.md#result_provider_registration) for more details + +### Search completion cause + +There is currently no way to identify `globalSearch.find` observable completion cause: +searches completing because all providers returned all their results and searches +completing because the consumer aborted the search using the `aborted$` option or because +the internal timout period has been reaches will both complete the same way. diff --git a/x-pack/plugins/global_search/common/errors.test.ts b/x-pack/plugins/global_search/common/errors.test.ts new file mode 100644 index 0000000000000..949795abd701a --- /dev/null +++ b/x-pack/plugins/global_search/common/errors.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GlobalSearchFindError } from './errors'; + +describe('GlobalSearchFindError', () => { + describe('#invalidLicense', () => { + it('create an error with the correct `type`', () => { + const error = GlobalSearchFindError.invalidLicense('foobar'); + expect(error.message).toBe('foobar'); + expect(error.type).toBe('invalid-license'); + }); + + it('can be identified via instanceof', () => { + const error = GlobalSearchFindError.invalidLicense('foo'); + expect(error instanceof GlobalSearchFindError).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/global_search/common/errors.ts b/x-pack/plugins/global_search/common/errors.ts new file mode 100644 index 0000000000000..15bc0958cb8aa --- /dev/null +++ b/x-pack/plugins/global_search/common/errors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// only one type for now, but already present for future-proof reasons +export type GlobalSearchFindErrorType = 'invalid-license'; + +/** + * Error thrown from the {@link GlobalSearchPluginStart.find | GlobalSearch find API}'s result observable + * + * @public + */ +export class GlobalSearchFindError extends Error { + public static invalidLicense(message: string) { + return new GlobalSearchFindError('invalid-license', message); + } + + private constructor(public readonly type: GlobalSearchFindErrorType, message: string) { + super(message); + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, GlobalSearchFindError.prototype); + } +} diff --git a/x-pack/plugins/global_search/common/license_checker.mock.ts b/x-pack/plugins/global_search/common/license_checker.mock.ts new file mode 100644 index 0000000000000..e19a2562e53d8 --- /dev/null +++ b/x-pack/plugins/global_search/common/license_checker.mock.ts @@ -0,0 +1,24 @@ +/* + * 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 { ILicenseChecker } from './license_checker'; + +const createLicenseCheckerMock = (): jest.Mocked => { + const mock = { + getState: jest.fn(), + getLicense: jest.fn(), + clean: jest.fn(), + }; + + mock.getLicense.mockReturnValue(undefined); + mock.getState.mockReturnValue({ valid: true }); + + return mock; +}; + +export const licenseCheckerMock = { + create: createLicenseCheckerMock, +}; diff --git a/x-pack/plugins/global_search/common/license_checker.test.ts b/x-pack/plugins/global_search/common/license_checker.test.ts new file mode 100644 index 0000000000000..47a0d41016d71 --- /dev/null +++ b/x-pack/plugins/global_search/common/license_checker.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable, of, BehaviorSubject } from 'rxjs'; +import { licenseMock } from '../../licensing/common/licensing.mock'; +import { ILicense, LicenseCheck } from '../../licensing/common/types'; +import { LicenseChecker } from './license_checker'; + +describe('LicenseChecker', () => { + const createLicense = (check: LicenseCheck): ILicense => { + const license = licenseMock.createLicenseMock(); + license.check.mockReturnValue(check); + return license; + }; + + const createLicense$ = (check: LicenseCheck): Observable => of(createLicense(check)); + + it('returns the correct state of the license', () => { + let checker = new LicenseChecker(createLicense$({ state: 'valid' })); + expect(checker.getState()).toEqual({ valid: true }); + + checker = new LicenseChecker(createLicense$({ state: 'expired' })); + expect(checker.getState()).toEqual({ valid: false, message: 'expired' }); + + checker = new LicenseChecker(createLicense$({ state: 'invalid' })); + expect(checker.getState()).toEqual({ valid: false, message: 'invalid' }); + + checker = new LicenseChecker(createLicense$({ state: 'unavailable' })); + expect(checker.getState()).toEqual({ valid: false, message: 'unavailable' }); + }); + + it('updates the state when the license changes', () => { + const license$ = new BehaviorSubject(createLicense({ state: 'valid' })); + + const checker = new LicenseChecker(license$); + expect(checker.getState()).toEqual({ valid: true }); + + license$.next(createLicense({ state: 'expired' })); + expect(checker.getState()).toEqual({ valid: false, message: 'expired' }); + + license$.next(createLicense({ state: 'valid' })); + expect(checker.getState()).toEqual({ valid: true }); + }); + + it('removes the subscription when calling `clean`', () => { + const mockUnsubscribe = jest.fn(); + const mockObs = { + subscribe: jest.fn().mockReturnValue({ unsubscribe: mockUnsubscribe }), + }; + + const checker = new LicenseChecker(mockObs as any); + + expect(mockObs.subscribe).toHaveBeenCalledTimes(1); + expect(mockUnsubscribe).not.toHaveBeenCalled(); + + checker.clean(); + + expect(mockUnsubscribe).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/global_search/common/license_checker.ts b/x-pack/plugins/global_search/common/license_checker.ts new file mode 100644 index 0000000000000..d201b31802b32 --- /dev/null +++ b/x-pack/plugins/global_search/common/license_checker.ts @@ -0,0 +1,49 @@ +/* + * 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 { Observable, Subscription } from 'rxjs'; +import { ILicense } from '../../licensing/common/types'; + +export type LicenseState = { valid: false; message: string } | { valid: true }; + +export type CheckLicense = (license: ILicense) => LicenseState; + +const checkLicense: CheckLicense = (license) => { + const check = license.check('globalSearch', 'basic'); + switch (check.state) { + case 'expired': + return { valid: false, message: 'expired' }; + case 'invalid': + return { valid: false, message: 'invalid' }; + case 'unavailable': + return { valid: false, message: 'unavailable' }; + case 'valid': + return { valid: true }; + default: + throw new Error(`Invalid license state: ${check.state}`); + } +}; + +export type ILicenseChecker = PublicMethodsOf; + +export class LicenseChecker { + private subscription: Subscription; + private state: LicenseState = { valid: false, message: 'unknown' }; + + constructor(license$: Observable) { + this.subscription = license$.subscribe((license) => { + this.state = checkLicense(license); + }); + } + + public getState() { + return this.state; + } + + public clean() { + this.subscription.unsubscribe(); + } +} diff --git a/x-pack/plugins/global_search/common/operators/index.ts b/x-pack/plugins/global_search/common/operators/index.ts new file mode 100644 index 0000000000000..2a0cf066a04aa --- /dev/null +++ b/x-pack/plugins/global_search/common/operators/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { takeInArray } from './take_in_array'; diff --git a/x-pack/plugins/global_search/common/operators/take_in_array.test.ts b/x-pack/plugins/global_search/common/operators/take_in_array.test.ts new file mode 100644 index 0000000000000..b73ee20c9889a --- /dev/null +++ b/x-pack/plugins/global_search/common/operators/take_in_array.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { TestScheduler } from 'rxjs/testing'; +import { takeInArray } from './take_in_array'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + +describe('takeInArray', () => { + it('only emits a given `count` of items from an array observable', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const source = hot('a-b-c', { a: [1], b: [2], c: [3] }); + const expected = 'a-(b|)'; + + expectObservable(source.pipe(takeInArray(2))).toBe(expected, { + a: [1], + b: [2], + }); + }); + }); + + it('completes if the source completes before reaching the given `count`', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const source = hot('a-b-c-|', { a: [1, 2], b: [3, 4], c: [5] }); + const expected = 'a-b-c-|'; + + expectObservable(source.pipe(takeInArray(10))).toBe(expected, { + a: [1, 2], + b: [3, 4], + c: [5], + }); + }); + }); + + it('split the emission if `count` is reached in a given emission', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const source = hot('a-b-c', { a: [1, 2, 3], b: [4, 5, 6], c: [7, 8] }); + const expected = 'a-(b|)'; + + expectObservable(source.pipe(takeInArray(5))).toBe(expected, { + a: [1, 2, 3], + b: [4, 5], + }); + }); + }); + + it('throws when trying to take a negative number of items', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const source = hot('a-b-c', { a: [1, 2, 3], b: [4, 5, 6], c: [7, 8] }); + + expect(() => { + source.pipe(takeInArray(-4)).subscribe(() => undefined); + }).toThrowErrorMatchingInlineSnapshot(`"Cannot take a negative number of items"`); + }); + }); +}); diff --git a/x-pack/plugins/global_search/common/operators/take_in_array.ts b/x-pack/plugins/global_search/common/operators/take_in_array.ts new file mode 100644 index 0000000000000..7d041d3c2bab0 --- /dev/null +++ b/x-pack/plugins/global_search/common/operators/take_in_array.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ + +// eslint-disable-next-line max-classes-per-file +import { + EMPTY, + MonoTypeOperatorFunction, + Observable, + Operator, + Subscriber, + TeardownLogic, +} from 'rxjs'; + +/** + * Emits only the first `count` items from the arrays emitted by the source Observable. The limit + * is global to all emitted values, and not per emission. + * + * @example + * ```ts + * const source = of([1, 2], [3, 4], [5, 6]); + * const takeThreeInArray = source.pipe(takeInArray(3)); + * takeThreeInArray.subscribe(x => console.log(x)); + * + * // Logs: + * // [1,2] + * // [3] + * ``` + * + * @param count The total maximum number of value to keep from the emitted arrays + */ +export function takeInArray(count: number): MonoTypeOperatorFunction { + return function takeLastOperatorFunction(source: Observable): Observable { + if (count === 0) { + return EMPTY; + } else { + return source.lift(new TakeInArray(count)); + } + }; +} + +class TakeInArray implements Operator { + constructor(private total: number) { + if (this.total < 0) { + throw new Error('Cannot take a negative number of items'); + } + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new TakeInArraySubscriber(subscriber, this.total)); + } +} + +class TakeInArraySubscriber extends Subscriber { + private current: number = 0; + + constructor(destination: Subscriber, private total: number) { + super(destination); + } + + protected _next(value: T[]): void { + const remaining = this.total - this.current; + if (remaining > value.length) { + this.destination.next!(value); + this.current += value.length; + } else { + this.destination.next!(value.slice(0, remaining)); + this.destination.complete!(); + this.unsubscribe(); + } + } +} diff --git a/x-pack/plugins/global_search/common/process_result.test.mocks.ts b/x-pack/plugins/global_search/common/process_result.test.mocks.ts new file mode 100644 index 0000000000000..718ac7a1a6a52 --- /dev/null +++ b/x-pack/plugins/global_search/common/process_result.test.mocks.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const convertResultUrlMock = jest.fn().mockReturnValue('converted-url'); +jest.doMock('./utils', () => ({ + convertResultUrl: convertResultUrlMock, +})); diff --git a/x-pack/plugins/global_search/common/process_result.test.ts b/x-pack/plugins/global_search/common/process_result.test.ts new file mode 100644 index 0000000000000..723f21a24f552 --- /dev/null +++ b/x-pack/plugins/global_search/common/process_result.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { convertResultUrlMock } from './process_result.test.mocks'; + +import { IBasePath } from './utils'; +import { GlobalSearchProviderResult } from './types'; +import { processProviderResult } from './process_result'; + +const createResult = (parts: Partial): GlobalSearchProviderResult => ({ + id: 'id', + title: 'title', + type: 'type', + icon: 'icon', + url: '/foo/bar', + score: 42, + meta: { foo: 'bar' }, + ...parts, +}); + +describe('processProviderResult', () => { + let basePath: jest.Mocked; + + beforeEach(() => { + basePath = { + prepend: jest.fn(), + }; + + convertResultUrlMock.mockClear(); + }); + + it('returns all properties unchanged except `url`', () => { + const r1 = createResult({ + id: '1', + type: 'test', + url: '/url-1', + title: 'title 1', + icon: 'foo', + score: 69, + meta: { hello: 'dolly' }, + }); + + expect(processProviderResult(r1, basePath)).toEqual({ + ...r1, + url: expect.any(String), + }); + }); + + it('converts the url using `convertResultUrl`', () => { + const r1 = createResult({ id: '1', url: '/url-1' }); + const r2 = createResult({ id: '2', url: '/url-2' }); + + convertResultUrlMock.mockReturnValueOnce('/url-A'); + convertResultUrlMock.mockReturnValueOnce('/url-B'); + + expect(convertResultUrlMock).not.toHaveBeenCalled(); + + const g1 = processProviderResult(r1, basePath); + + expect(g1.url).toEqual('/url-A'); + expect(convertResultUrlMock).toHaveBeenCalledTimes(1); + expect(convertResultUrlMock).toHaveBeenCalledWith(r1.url, basePath); + + const g2 = processProviderResult(r2, basePath); + + expect(g2.url).toEqual('/url-B'); + expect(convertResultUrlMock).toHaveBeenCalledTimes(2); + expect(convertResultUrlMock).toHaveBeenCalledWith(r2.url, basePath); + }); +}); diff --git a/x-pack/plugins/global_search/common/process_result.ts b/x-pack/plugins/global_search/common/process_result.ts new file mode 100644 index 0000000000000..fed6dc14f066b --- /dev/null +++ b/x-pack/plugins/global_search/common/process_result.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GlobalSearchProviderResult, GlobalSearchResult } from './types'; +import { convertResultUrl, IBasePath } from './utils'; + +/** + * Convert a {@link GlobalSearchProviderResult | provider result} + * to a {@link GlobalSearchResult | service result} + */ +export const processProviderResult = ( + result: GlobalSearchProviderResult, + basePath: IBasePath +): GlobalSearchResult => { + return { + ...result, + url: convertResultUrl(result.url, basePath), + }; +}; diff --git a/x-pack/plugins/global_search/common/types.ts b/x-pack/plugins/global_search/common/types.ts new file mode 100644 index 0000000000000..26940806a4ecd --- /dev/null +++ b/x-pack/plugins/global_search/common/types.ts @@ -0,0 +1,89 @@ +/* + * 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 { Observable } from 'rxjs'; +import { Serializable } from 'src/core/types'; + +/** + * Options provided to {@link GlobalSearchResultProvider | a result provider}'s `find` method. + */ +export interface GlobalSearchProviderFindOptions { + /** + * A custom preference token associated with a search 'session' that should be used to get consistent scoring + * when performing calls to ES. Can also be used as a 'session' token for providers returning data from elsewhere + * than an elasticsearch cluster. + */ + preference: string; + /** + * Observable that emits once if and when the `find` call has been aborted, either manually by the consumer, + * or when the internal timeout period as been reached. + * + * When a `find` request is effectively aborted, the service will stop emitting any new result to the consumer anyway, but + * this can (and should) be used to cancel any pending asynchronous task and complete the result observable from within the provider. + */ + aborted$: Observable; + /** + * The total maximum number of results (including all batches, not per emission) that should be returned by the provider for a given `find` request. + * Any result emitted exceeding this quota will be ignored by the service and not emitted to the consumer. + */ + maxResults: number; +} + +/** + * Structured type for the {@link GlobalSearchProviderResult.url | provider result's url property} + */ +export type GlobalSearchProviderResultUrl = string | { path: string; prependBasePath: boolean }; + +/** + * Representation of a result returned by a {@link GlobalSearchResultProvider | result provider} + */ +export interface GlobalSearchProviderResult { + /** an id that should be unique for an individual provider's results */ + id: string; + /** the title/label of the result */ + title: string; + /** the type of result */ + type: string; + /** an optional EUI icon name to associate with the search result */ + icon?: string; + /** + * The url associated with this result. + * This can be either an absolute url, a path relative to the basePath, or a structure specifying if the basePath should be prepended. + * + * @example + * `result.url = 'https://kibana-instance:8080/base-path/app/my-app/my-result-type/id';` + * `result.url = '/app/my-app/my-result-type/id';` + * `result.url = { path: '/base-path/app/my-app/my-result-type/id', prependBasePath: false };` + */ + url: GlobalSearchProviderResultUrl; + /** the score of the result, from 1 (lowest) to 100 (highest) */ + score: number; + /** an optional record of metadata for this result */ + meta?: Record; +} + +/** + * Representation of a result returned by the {@link GlobalSearchPluginStart.find | `find` API} + */ +export type GlobalSearchResult = Omit & { + /** + * The url associated with this result. + * This can be either an absolute url, or a relative path including the basePath + */ + url: string; +}; + +/** + * Response returned from the {@link GlobalSearchPluginStart | global search service}'s `find` API + * + * @public + */ +export interface GlobalSearchBatchedResults { + /** + * Results for this batch + */ + results: GlobalSearchResult[]; +} diff --git a/x-pack/plugins/global_search/common/utils.test.ts b/x-pack/plugins/global_search/common/utils.test.ts new file mode 100644 index 0000000000000..27f1ce99a58cc --- /dev/null +++ b/x-pack/plugins/global_search/common/utils.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { convertResultUrl } from './utils'; + +const createBasePath = () => ({ + prepend: jest.fn(), +}); + +describe('convertResultUrl', () => { + let basePath: ReturnType; + + beforeEach(() => { + basePath = createBasePath(); + basePath.prepend.mockImplementation((path) => `/base-path${path}`); + }); + + describe('when the url is a string', () => { + it('does not convert absolute urls', () => { + expect(convertResultUrl('http://kibana:8080/foo/bar', basePath)).toEqual( + 'http://kibana:8080/foo/bar' + ); + expect(convertResultUrl('https://localhost/path/to/thing', basePath)).toEqual( + 'https://localhost/path/to/thing' + ); + expect(basePath.prepend).toHaveBeenCalledTimes(0); + }); + + it('prepends the base path to relative urls', () => { + expect(convertResultUrl('/app/my-app/foo', basePath)).toEqual('/base-path/app/my-app/foo'); + expect(basePath.prepend).toHaveBeenCalledTimes(1); + expect(basePath.prepend).toHaveBeenCalledWith('/app/my-app/foo'); + + expect(convertResultUrl('/some-path', basePath)).toEqual('/base-path/some-path'); + expect(basePath.prepend).toHaveBeenCalledTimes(2); + expect(basePath.prepend).toHaveBeenCalledWith('/some-path'); + }); + }); + + describe('when the url is an object', () => { + it('converts the path if `prependBasePath` is true', () => { + expect(convertResultUrl({ path: '/app/my-app', prependBasePath: true }, basePath)).toEqual( + '/base-path/app/my-app' + ); + expect(basePath.prepend).toHaveBeenCalledTimes(1); + expect(basePath.prepend).toHaveBeenCalledWith('/app/my-app'); + + expect(convertResultUrl({ path: '/some-path', prependBasePath: true }, basePath)).toEqual( + '/base-path/some-path' + ); + expect(basePath.prepend).toHaveBeenCalledTimes(2); + expect(basePath.prepend).toHaveBeenCalledWith('/some-path'); + }); + it('does not convert the path if `prependBasePath` is false', () => { + expect(convertResultUrl({ path: '/app/my-app', prependBasePath: false }, basePath)).toEqual( + '/app/my-app' + ); + expect(convertResultUrl({ path: '/some-path', prependBasePath: false }, basePath)).toEqual( + '/some-path' + ); + expect(basePath.prepend).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/x-pack/plugins/global_search/common/utils.ts b/x-pack/plugins/global_search/common/utils.ts new file mode 100644 index 0000000000000..46648319458de --- /dev/null +++ b/x-pack/plugins/global_search/common/utils.ts @@ -0,0 +1,35 @@ +/* + * 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 { GlobalSearchProviderResultUrl } from './types'; + +// interface matching both the server and client-side implementation of IBasePath for our needs +// used to avoid duplicating `convertResultUrl` in server and client code due to different signatures. +export interface IBasePath { + prepend(path: string): string; +} + +/** + * Convert a {@link GlobalSearchProviderResultUrl | provider result's url} to an absolute or relative url + * usable in {@link GlobalSearchResult | service results} + */ +export const convertResultUrl = ( + url: GlobalSearchProviderResultUrl, + basePath: IBasePath +): string => { + if (typeof url === 'string') { + // relative path + if (url.startsWith('/')) { + return basePath.prepend(url); + } + // absolute url + return url; + } + if (url.prependBasePath) { + return basePath.prepend(url.path); + } + return url.path; +}; diff --git a/x-pack/plugins/global_search/kibana.json b/x-pack/plugins/global_search/kibana.json new file mode 100644 index 0000000000000..c94e080a8c589 --- /dev/null +++ b/x-pack/plugins/global_search/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "globalSearch", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["licensing"], + "optionalPlugins": [], + "configPath": ["xpack", "global_search"] +} diff --git a/x-pack/plugins/global_search/public/config.ts b/x-pack/plugins/global_search/public/config.ts new file mode 100644 index 0000000000000..a3969bef287b2 --- /dev/null +++ b/x-pack/plugins/global_search/public/config.ts @@ -0,0 +1,12 @@ +/* + * 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 interface GlobalSearchClientConfigType { + // is a string because the server-side counterpart is a duration + // which is serialized to string when sent to the client + // should be parsed using moment.duration(config.search_timeout) + search_timeout: string; +} diff --git a/x-pack/plugins/global_search/public/index.ts b/x-pack/plugins/global_search/public/index.ts new file mode 100644 index 0000000000000..18483cea72540 --- /dev/null +++ b/x-pack/plugins/global_search/public/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { PluginInitializer } from 'src/core/public'; +import { + GlobalSearchPlugin, + GlobalSearchPluginSetupDeps, + GlobalSearchPluginStartDeps, +} from './plugin'; +import { GlobalSearchPluginSetup, GlobalSearchPluginStart } from './types'; + +export const plugin: PluginInitializer< + GlobalSearchPluginSetup, + GlobalSearchPluginStart, + GlobalSearchPluginSetupDeps, + GlobalSearchPluginStartDeps +> = (context) => new GlobalSearchPlugin(context); + +export { + GlobalSearchBatchedResults, + GlobalSearchProviderFindOptions, + GlobalSearchProviderResult, + GlobalSearchProviderResultUrl, + GlobalSearchResult, +} from '../common/types'; +export { + GlobalSearchPluginSetup, + GlobalSearchPluginStart, + GlobalSearchResultProvider, +} from './types'; +export { GlobalSearchFindOptions } from './services/types'; diff --git a/x-pack/plugins/global_search/public/mocks.ts b/x-pack/plugins/global_search/public/mocks.ts new file mode 100644 index 0000000000000..97dc01e92dbfe --- /dev/null +++ b/x-pack/plugins/global_search/public/mocks.ts @@ -0,0 +1,29 @@ +/* + * 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 { GlobalSearchPluginSetup, GlobalSearchPluginStart } from './types'; +import { searchServiceMock } from './services/search_service.mock'; + +const createSetupMock = (): jest.Mocked => { + const searchMock = searchServiceMock.createSetupContract(); + + return { + registerResultProvider: searchMock.registerResultProvider, + }; +}; + +const createStartMock = (): jest.Mocked => { + const searchMock = searchServiceMock.createStartContract(); + + return { + find: searchMock.find, + }; +}; + +export const globalSearchPluginMock = { + createSetupContract: createSetupMock, + createStartContract: createStartMock, +}; diff --git a/x-pack/plugins/global_search/public/plugin.ts b/x-pack/plugins/global_search/public/plugin.ts new file mode 100644 index 0000000000000..6af8ec32a581d --- /dev/null +++ b/x-pack/plugins/global_search/public/plugin.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { LicensingPluginStart } from '../../licensing/public'; +import { LicenseChecker, ILicenseChecker } from '../common/license_checker'; +import { GlobalSearchPluginSetup, GlobalSearchPluginStart } from './types'; +import { GlobalSearchClientConfigType } from './config'; +import { SearchService } from './services'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface GlobalSearchPluginSetupDeps {} +export interface GlobalSearchPluginStartDeps { + licensing: LicensingPluginStart; +} + +export class GlobalSearchPlugin + implements + Plugin< + GlobalSearchPluginSetup, + GlobalSearchPluginStart, + GlobalSearchPluginSetupDeps, + GlobalSearchPluginStartDeps + > { + private readonly config: GlobalSearchClientConfigType; + private licenseChecker?: ILicenseChecker; + private readonly searchService = new SearchService(); + + constructor(context: PluginInitializerContext) { + this.config = context.config.get(); + } + + setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { + const { registerResultProvider } = this.searchService.setup({ + config: this.config, + }); + + return { + registerResultProvider, + }; + } + + start({ http }: CoreStart, { licensing }: GlobalSearchPluginStartDeps) { + this.licenseChecker = new LicenseChecker(licensing.license$); + const { find } = this.searchService.start({ + http, + licenseChecker: this.licenseChecker, + }); + + return { + find, + }; + } + + public stop() { + if (this.licenseChecker) { + this.licenseChecker.clean(); + } + } +} diff --git a/x-pack/plugins/global_search/public/services/fetch_server_results.test.ts b/x-pack/plugins/global_search/public/services/fetch_server_results.test.ts new file mode 100644 index 0000000000000..f62acd08633ff --- /dev/null +++ b/x-pack/plugins/global_search/public/services/fetch_server_results.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { TestScheduler } from 'rxjs/testing'; +import { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { GlobalSearchResult } from '../../common/types'; +import { fetchServerResults } from './fetch_server_results'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + +const createResult = (id: string, parts: Partial = {}): GlobalSearchResult => ({ + id, + title: id, + type: 'type', + url: `/path/to/${id}`, + score: 100, + ...parts, +}); + +describe('fetchServerResults', () => { + let http: ReturnType; + + beforeEach(() => { + http = httpServiceMock.createStartContract(); + }); + + it('perform a POST request to the endpoint with valid options', () => { + http.post.mockResolvedValue({ results: [] }); + + fetchServerResults(http, 'some term', { preference: 'pref' }); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/global_search/find', { + body: JSON.stringify({ term: 'some term', options: { preference: 'pref' } }), + }); + }); + + it('returns the results from the server', async () => { + const resultA = createResult('A'); + const resultB = createResult('B'); + + http.post.mockResolvedValue({ results: [resultA, resultB] }); + + const results = await fetchServerResults(http, 'some term', { preference: 'pref' }).toPromise(); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(results).toHaveLength(2); + expect(results[0]).toEqual(resultA); + expect(results[1]).toEqual(resultB); + }); + + describe('returns an observable that', () => { + // NOTE: test scheduler do not properly work with promises because of their asynchronous nature. + // we are cheating here by having `http.post` return an observable instead of a promise. + // this still allows more finely grained testing about timing, and asserting that the method + // works properly when `post` returns a real promise is handled in other tests of this suite + + it('emits when the response is received', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + http.post.mockReturnValue(hot('---(a|)', { a: { results: [] } }) as any); + + const results = fetchServerResults(http, 'term', {}); + + expectObservable(results).toBe('---(a|)', { + a: [], + }); + }); + }); + + it('completes without returning results if aborted$ emits before the response', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + http.post.mockReturnValue(hot('---(a|)', { a: { results: [] } }) as any); + const aborted$ = hot('-(a|)', { a: undefined }); + const results = fetchServerResults(http, 'term', { aborted$ }); + + expectObservable(results).toBe('-|', { + a: [], + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/global_search/public/services/fetch_server_results.ts b/x-pack/plugins/global_search/public/services/fetch_server_results.ts new file mode 100644 index 0000000000000..3c06dfab9f50e --- /dev/null +++ b/x-pack/plugins/global_search/public/services/fetch_server_results.ts @@ -0,0 +1,46 @@ +/* + * 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 { Observable, from, EMPTY } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { HttpStart } from 'src/core/public'; +import { GlobalSearchResult } from '../../common/types'; +import { GlobalSearchFindOptions } from './types'; + +interface ServerFetchResponse { + results: GlobalSearchResult[]; +} + +/** + * Fetch the server-side results from the GS internal HTTP API. + * + * @remarks + * Though this function returns an Observable, the current implementation is not streaming + * results from the server. All results will be returned in a single batch when + * all server-side providers are completed. + */ +export const fetchServerResults = ( + http: HttpStart, + term: string, + { preference, aborted$ }: GlobalSearchFindOptions +): Observable => { + let controller: AbortController | undefined; + if (aborted$) { + controller = new AbortController(); + aborted$.subscribe(() => { + controller!.abort(); + }); + } + return from( + http.post('/internal/global_search/find', { + body: JSON.stringify({ term, options: { preference } }), + signal: controller?.signal, + }) + ).pipe( + takeUntil(aborted$ ?? EMPTY), + map((response) => response.results) + ); +}; diff --git a/x-pack/plugins/global_search/public/services/index.ts b/x-pack/plugins/global_search/public/services/index.ts new file mode 100644 index 0000000000000..8d3cb86043432 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { SearchService, SearchServiceSetup, SearchServiceStart } from './search_service'; +export { GlobalSearchFindOptions } from './types'; diff --git a/x-pack/plugins/global_search/public/services/search_service.mock.ts b/x-pack/plugins/global_search/public/services/search_service.mock.ts new file mode 100644 index 0000000000000..eca69148288b9 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/search_service.mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchServiceSetup, SearchServiceStart } from './search_service'; +import { of } from 'rxjs'; + +const createSetupMock = (): jest.Mocked => { + return { + registerResultProvider: jest.fn(), + }; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + find: jest.fn(), + }; + mock.find.mockReturnValue(of({ results: [] })); + + return mock; +}; + +export const searchServiceMock = { + createSetupContract: createSetupMock, + createStartContract: createStartMock, +}; diff --git a/x-pack/plugins/global_search/public/services/search_service.test.mocks.ts b/x-pack/plugins/global_search/public/services/search_service.test.mocks.ts new file mode 100644 index 0000000000000..ce406e27c4a72 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/search_service.test.mocks.ts @@ -0,0 +1,16 @@ +/* + * 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 fetchServerResultsMock = jest.fn(); +jest.doMock('./fetch_server_results', () => ({ + fetchServerResults: fetchServerResultsMock, +})); + +export const getDefaultPreferenceMock = jest.fn(); +jest.doMock('./utils', () => ({ + ...jest.requireActual('./utils'), + getDefaultPreference: getDefaultPreferenceMock, +})); diff --git a/x-pack/plugins/global_search/public/services/search_service.test.ts b/x-pack/plugins/global_search/public/services/search_service.test.ts new file mode 100644 index 0000000000000..350547a928fe4 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/search_service.test.ts @@ -0,0 +1,436 @@ +/* + * 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 { fetchServerResultsMock, getDefaultPreferenceMock } from './search_service.test.mocks'; + +import { Observable, of } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; +import { duration } from 'moment'; +import { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { licenseCheckerMock } from '../../common/license_checker.mock'; +import { GlobalSearchProviderResult, GlobalSearchResult } from '../../common/types'; +import { GlobalSearchFindError } from '../../common/errors'; +import { GlobalSearchClientConfigType } from '../config'; +import { GlobalSearchResultProvider } from '../types'; +import { SearchService } from './search_service'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + +describe('SearchService', () => { + let service: SearchService; + let httpStart: ReturnType; + let licenseChecker: ReturnType; + + const createConfig = (timeoutMs: number = 30000): GlobalSearchClientConfigType => { + return { + search_timeout: duration(timeoutMs).toString(), + }; + }; + + const startDeps = () => ({ + http: httpStart, + licenseChecker, + }); + + const createProvider = ( + id: string, + source: Observable = of([]) + ): jest.Mocked => ({ + id, + find: jest.fn().mockImplementation((term, options, context) => source), + }); + + const expectedResult = (id: string) => expect.objectContaining({ id }); + + const expectedBatch = (...ids: string[]) => ({ + results: ids.map((id) => expectedResult(id)), + }); + + const providerResult = ( + id: string, + parts: Partial = {} + ): GlobalSearchProviderResult => ({ + title: id, + type: 'test', + url: '/foo/bar', + score: 100, + ...parts, + id, + }); + + const serverResult = ( + id: string, + parts: Partial = {} + ): GlobalSearchResult => ({ + title: id, + type: 'test', + url: '/foo/bar', + score: 100, + ...parts, + id, + }); + + beforeEach(() => { + service = new SearchService(); + httpStart = httpServiceMock.createStartContract({ basePath: '/base-path' }); + licenseChecker = licenseCheckerMock.create(); + + fetchServerResultsMock.mockClear(); + fetchServerResultsMock.mockReturnValue(of()); + + getDefaultPreferenceMock.mockClear(); + getDefaultPreferenceMock.mockReturnValue('default_pref'); + }); + + describe('#setup()', () => { + describe('#registerResultProvider()', () => { + it('throws when trying to register the same provider twice', () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const provider = createProvider('A'); + registerResultProvider(provider); + expect(() => { + registerResultProvider(provider); + }).toThrowErrorMatchingInlineSnapshot(`"trying to register duplicate provider: A"`); + }); + }); + }); + + describe('#start()', () => { + describe('#find()', () => { + it('calls the provider with the correct parameters', () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const provider = createProvider('A'); + registerResultProvider(provider); + + const { find } = service.start(startDeps()); + find('foobar', { preference: 'pref' }); + + expect(provider.find).toHaveBeenCalledTimes(1); + expect(provider.find).toHaveBeenCalledWith( + 'foobar', + expect.objectContaining({ preference: 'pref' }) + ); + }); + + it('calls `fetchServerResults` with the correct parameters', () => { + service.setup({ config: createConfig() }); + + const { find } = service.start(startDeps()); + find('foobar', { preference: 'pref' }); + + expect(fetchServerResultsMock).toHaveBeenCalledTimes(1); + expect(fetchServerResultsMock).toHaveBeenCalledWith( + httpStart, + 'foobar', + expect.objectContaining({ preference: 'pref', aborted$: expect.any(Object) }) + ); + }); + + it('calls `getDefaultPreference` when `preference` is not specified', () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const provider = createProvider('A'); + registerResultProvider(provider); + + const { find } = service.start(startDeps()); + find('foobar', { preference: 'pref' }); + + expect(getDefaultPreferenceMock).not.toHaveBeenCalled(); + + expect(provider.find).toHaveBeenNthCalledWith( + 1, + 'foobar', + expect.objectContaining({ + preference: 'pref', + }) + ); + + find('foobar', {}); + + expect(getDefaultPreferenceMock).toHaveBeenCalledTimes(1); + + expect(provider.find).toHaveBeenNthCalledWith( + 2, + 'foobar', + expect.objectContaining({ + preference: 'default_pref', + }) + ); + }); + + it('return the results from the provider', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('a-b-|', { + a: [providerResult('1')], + b: [providerResult('2')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const { find } = service.start(startDeps()); + const results = find('foo', {}); + + expectObservable(results).toBe('a-b-|', { + a: expectedBatch('1'), + b: expectedBatch('2'), + }); + }); + }); + + it('return the results from the server', async () => { + service.setup({ config: createConfig() }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const serverResults = hot('a-b-|', { + a: [serverResult('1')], + b: [serverResult('2')], + }); + + fetchServerResultsMock.mockReturnValue(serverResults); + + const { find } = service.start(startDeps()); + const results = find('foo', {}); + + expectObservable(results).toBe('a-b-|', { + a: expectedBatch('1'), + b: expectedBatch('2'), + }); + }); + }); + + it('handles multiple providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider( + 'A', + hot('a---d-|', { + a: [providerResult('A1'), providerResult('A2')], + d: [providerResult('A3')], + }) + ) + ); + registerResultProvider( + createProvider( + 'B', + hot('-b-c| ', { + b: [providerResult('B1')], + c: [providerResult('B2'), providerResult('B3')], + }) + ) + ); + + const { find } = service.start(startDeps()); + const results = find('foo', {}); + + expectObservable(results).toBe('ab-cd-|', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('B2', 'B3'), + d: expectedBatch('A3'), + }); + }); + }); + + it('return mixed server/client providers results', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + fetchServerResultsMock.mockReturnValue( + hot('-----(c|)', { + c: [serverResult('S1'), serverResult('S2')], + }) + ); + + registerResultProvider( + createProvider( + 'A', + hot('a-b-|', { + a: [providerResult('P1')], + b: [providerResult('P2')], + }) + ) + ); + + const { find } = service.start(startDeps()); + const results = find('foo', {}); + + expectObservable(results).toBe('a-b--(c|)', { + a: expectedBatch('P1'), + b: expectedBatch('P2'), + c: expectedBatch('S1', 'S2'), + }); + }); + }); + + it('handles the `aborted$` option', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('--a---(b|)', { + a: [providerResult('1')], + b: [providerResult('2')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const aborted$ = hot('----a--|', { a: undefined }); + + const { find } = service.start(startDeps()); + const results = find('foo', { aborted$ }); + + expectObservable(results).toBe('--a-|', { + a: expectedBatch('1'), + }); + }); + }); + + it('respects the timeout duration', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(100), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('a 24ms b 100ms (c|)', { + a: [providerResult('1')], + b: [providerResult('2')], + c: [providerResult('3')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const { find } = service.start(startDeps()); + const results = find('foo', {}); + + expectObservable(results).toBe('a 24ms b 74ms |', { + a: expectedBatch('1'), + b: expectedBatch('2'), + }); + }); + }); + + it('only returns a given maximum number of results per provider', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(100), + maxProviderResults: 2, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider( + 'A', + hot('a---d-|', { + a: [providerResult('A1'), providerResult('A2')], + d: [providerResult('A3')], + }) + ) + ); + registerResultProvider( + createProvider( + 'B', + hot('-b-c| ', { + b: [providerResult('B1')], + c: [providerResult('B2'), providerResult('B3')], + }) + ) + ); + + const { find } = service.start(startDeps()); + const results = find('foo', {}); + + expectObservable(results).toBe('ab-(c|)', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('B2'), + }); + }); + }); + + it('process the results before returning them', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const resultA = providerResult('A', { + type: 'application', + icon: 'appIcon', + score: 42, + title: 'foo', + url: '/foo/bar', + }); + const resultB = providerResult('B', { + type: 'dashboard', + score: 69, + title: 'bar', + url: { path: '/foo', prependBasePath: false }, + }); + + const provider = createProvider('A', of([resultA, resultB])); + registerResultProvider(provider); + + const { find } = service.start(startDeps()); + const batch = await find('foo', {}).pipe(take(1)).toPromise(); + + expect(batch.results).toHaveLength(2); + expect(batch.results[0]).toEqual({ + ...resultA, + url: '/base-path/foo/bar', + }); + expect(batch.results[1]).toEqual({ + ...resultB, + url: '/foo', + }); + }); + + it('emits an error when the license is invalid', async () => { + licenseChecker.getState.mockReturnValue({ valid: false, message: 'expired' }); + + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('a-b-|', { + a: [providerResult('1')], + b: [providerResult('2')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const { find } = service.start(startDeps()); + const results = find('foo', {}); + + expectObservable(results).toBe( + '#', + {}, + GlobalSearchFindError.invalidLicense( + 'GlobalSearch API is disabled because of invalid license state: expired' + ) + ); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/global_search/public/services/search_service.ts b/x-pack/plugins/global_search/public/services/search_service.ts new file mode 100644 index 0000000000000..68970b75ad975 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/search_service.ts @@ -0,0 +1,164 @@ +/* + * 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 { merge, Observable, timer, throwError } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { duration } from 'moment'; +import { i18n } from '@kbn/i18n'; +import { HttpStart } from 'src/core/public'; +import { GlobalSearchProviderResult, GlobalSearchBatchedResults } from '../../common/types'; +import { GlobalSearchFindError } from '../../common/errors'; +import { takeInArray } from '../../common/operators'; +import { processProviderResult } from '../../common/process_result'; +import { ILicenseChecker } from '../../common/license_checker'; +import { GlobalSearchResultProvider } from '../types'; +import { GlobalSearchClientConfigType } from '../config'; +import { GlobalSearchFindOptions } from './types'; +import { getDefaultPreference } from './utils'; +import { fetchServerResults } from './fetch_server_results'; + +/** @public */ +export interface SearchServiceSetup { + /** + * Register a result provider to be used by the search service. + * + * @example + * ```ts + * setupDeps.globalSearch.registerResultProvider({ + * id: 'my_provider', + * find: (term, options) => { + * const resultPromise = myService.search(term, options); + * return from(resultPromise).pipe(takeUntil(options.aborted$); + * }, + * }); + * ``` + * + * @remarks + * As results from providers registered from the client-side API will not be available from the server's `find` API, + * registering result providers from the client should only be done when returning results that would not be retrievable + * from the server-side. In any other situation, prefer registering your provider from the server-side instead. + */ + registerResultProvider(provider: GlobalSearchResultProvider): void; +} + +/** @public */ +export interface SearchServiceStart { + /** + * Perform a search for given `term` and {@link GlobalSearchFindOptions | options}. + * + * @example + * ```ts + * startDeps.globalSearch.find('some term').subscribe({ + * next: ({ results }) => { + * addNewResultsToList(results); + * }, + * error: () => {}, + * complete: () => { + * showAsyncSearchIndicator(false); + * } + * }); + * ``` + * + * @remarks + * Emissions from the resulting observable will only contains **new** results. It is the consumer's + * responsibility to aggregate the emission and sort the results if required. + */ + find(term: string, options: GlobalSearchFindOptions): Observable; +} + +interface SetupDeps { + config: GlobalSearchClientConfigType; + maxProviderResults?: number; +} + +interface StartDeps { + http: HttpStart; + licenseChecker: ILicenseChecker; +} + +const defaultMaxProviderResults = 20; +const mapToUndefined = () => undefined; + +/** @internal */ +export class SearchService { + private readonly providers = new Map(); + private config?: GlobalSearchClientConfigType; + private http?: HttpStart; + private maxProviderResults = defaultMaxProviderResults; + private licenseChecker?: ILicenseChecker; + + setup({ config, maxProviderResults = defaultMaxProviderResults }: SetupDeps): SearchServiceSetup { + this.config = config; + + this.maxProviderResults = maxProviderResults; + + return { + registerResultProvider: (provider) => { + if (this.providers.has(provider.id)) { + throw new Error(`trying to register duplicate provider: ${provider.id}`); + } + this.providers.set(provider.id, provider); + }, + }; + } + + start({ http, licenseChecker }: StartDeps): SearchServiceStart { + this.http = http; + this.licenseChecker = licenseChecker; + + return { + find: (term, options) => this.performFind(term, options), + }; + } + + private performFind(term: string, options: GlobalSearchFindOptions) { + const licenseState = this.licenseChecker!.getState(); + if (!licenseState.valid) { + return throwError( + GlobalSearchFindError.invalidLicense( + i18n.translate('xpack.globalSearch.find.invalidLicenseError', { + defaultMessage: `GlobalSearch API is disabled because of invalid license state: {errorMessage}`, + values: { errorMessage: licenseState.message }, + }) + ) + ); + } + + const timeout = duration(this.config!.search_timeout).asMilliseconds(); + const timeout$ = timer(timeout).pipe(map(mapToUndefined)); + const aborted$ = options.aborted$ ? merge(options.aborted$, timeout$) : timeout$; + const preference = options.preference ?? getDefaultPreference(); + + const providerOptions = { + ...options, + preference, + maxResults: this.maxProviderResults, + aborted$, + }; + + const processResult = (result: GlobalSearchProviderResult) => + processProviderResult(result, this.http!.basePath); + + const serverResults$ = fetchServerResults(this.http!, term, { + preference, + aborted$, + }); + + const providersResults$ = [...this.providers.values()].map((provider) => + provider.find(term, providerOptions).pipe( + takeInArray(this.maxProviderResults), + takeUntil(aborted$), + map((results) => results.map((r) => processResult(r))) + ) + ); + + return merge(...providersResults$, serverResults$).pipe( + map((results) => ({ + results, + })) + ); + } +} diff --git a/x-pack/plugins/global_search/public/services/types.ts b/x-pack/plugins/global_search/public/services/types.ts new file mode 100644 index 0000000000000..fcaa8f0545a6e --- /dev/null +++ b/x-pack/plugins/global_search/public/services/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; + +/** + * Options for the server-side {@link GlobalSearchPluginStart.find | find API} + */ +export interface GlobalSearchFindOptions { + /** + * A custom preference token associated with a search 'session' that should be used to get consistent scoring + * when performing calls to ES. Can also be used as a 'session' token for providers returning data from elsewhere + * than an elasticsearch cluster. + * + * If not specified, a random token will be generated and used. The token is stored in the sessionStorage and is guaranteed + * to be consistent during a given http 'session' + */ + preference?: string; + /** + * Optional observable to notify that the associated `find` call should be canceled. + * If/when provided and emitting, the result observable will be completed and no further result emission will be performed. + */ + aborted$?: Observable; +} diff --git a/x-pack/plugins/global_search/public/services/utils.test.ts b/x-pack/plugins/global_search/public/services/utils.test.ts new file mode 100644 index 0000000000000..f69fb1d2fd825 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/utils.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { StubBrowserStorage } from '../../../../../src/test_utils/public/stub_browser_storage'; +import { getDefaultPreference } from './utils'; + +describe('getDefaultPreference', () => { + let storage: Storage; + let getItemSpy: jest.SpyInstance; + let setItemSpy: jest.SpyInstance; + + beforeEach(() => { + storage = new StubBrowserStorage(); + getItemSpy = jest.spyOn(storage, 'getItem'); + setItemSpy = jest.spyOn(storage, 'setItem'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns the value in storage when available', () => { + getItemSpy.mockReturnValue('foo_pref'); + + const pref = getDefaultPreference(storage); + + expect(pref).toEqual('foo_pref'); + expect(getItemSpy).toHaveBeenCalledTimes(1); + expect(setItemSpy).not.toHaveBeenCalled(); + }); + + it('sets the value to the storage and return it when not already present', () => { + getItemSpy.mockReturnValue(null); + + const returnedPref = getDefaultPreference(storage); + + expect(getItemSpy).toHaveBeenCalledTimes(1); + expect(setItemSpy).toHaveBeenCalledTimes(1); + + const storedPref = setItemSpy.mock.calls[0][1]; + + expect(storage.length).toBe(1); + expect(storage.key(0)).toBe('globalSearch:defaultPref'); + expect(storedPref).toEqual(returnedPref); + }); +}); diff --git a/x-pack/plugins/global_search/public/services/utils.ts b/x-pack/plugins/global_search/public/services/utils.ts new file mode 100644 index 0000000000000..45d2ba7d7c210 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/utils.ts @@ -0,0 +1,24 @@ +/* + * 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 uuid from 'uuid'; + +const defaultPrefStorageKey = 'globalSearch:defaultPref'; + +/** + * Returns the default {@link GlobalSearchFindOptions.preference | preference} value. + * + * The implementation is based on the sessionStorage, which ensure the default value for a session/tab will remain the same. + */ +export const getDefaultPreference = (storage: Storage = window.sessionStorage): string => { + let pref = storage.getItem(defaultPrefStorageKey); + if (pref) { + return pref; + } + pref = uuid.v4(); + storage.setItem(defaultPrefStorageKey, pref); + return pref; +}; diff --git a/x-pack/plugins/global_search/public/types.ts b/x-pack/plugins/global_search/public/types.ts new file mode 100644 index 0000000000000..42ef234504d12 --- /dev/null +++ b/x-pack/plugins/global_search/public/types.ts @@ -0,0 +1,43 @@ +/* + * 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 { Observable } from 'rxjs'; +import { GlobalSearchProviderFindOptions, GlobalSearchProviderResult } from '../common/types'; +import { SearchServiceSetup, SearchServiceStart } from './services'; + +export type GlobalSearchPluginSetup = Pick; +export type GlobalSearchPluginStart = Pick; + +/** + * GlobalSearch result provider, to be registered using the {@link GlobalSearchPluginSetup | global search API} + */ +export interface GlobalSearchResultProvider { + /** + * id of the provider + */ + id: string; + /** + * Method that should return an observable used to emit new results from the provider. + * + * See {@GlobalSearchProviderResult | the result type} for the expected result structure. + * + * @example + * ```ts + * // returning all results in a single batch + * setupDeps.globalSearch.registerResultProvider({ + * id: 'my_provider', + * find: (term, { aborted$, preference, maxResults }, context) => { + * const resultPromise = myService.search(term, { preference, maxResults }, context.core.savedObjects.client); + * return from(resultPromise).pipe(takeUntil(aborted$)); + * }, + * }); + * ``` + */ + find( + term: string, + options: GlobalSearchProviderFindOptions + ): Observable; +} diff --git a/x-pack/plugins/global_search/server/config.ts b/x-pack/plugins/global_search/server/config.ts new file mode 100644 index 0000000000000..33ff45595b912 --- /dev/null +++ b/x-pack/plugins/global_search/server/config.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 { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + search_timeout: schema.duration({ defaultValue: '30s' }), +}); + +export type GlobalSearchConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + search_timeout: true, + }, +}; diff --git a/x-pack/plugins/global_search/server/index.ts b/x-pack/plugins/global_search/server/index.ts new file mode 100644 index 0000000000000..82f7c80dca552 --- /dev/null +++ b/x-pack/plugins/global_search/server/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from 'src/core/server'; +import { + GlobalSearchPlugin, + GlobalSearchPluginSetupDeps, + GlobalSearchPluginStartDeps, +} from './plugin'; +import { GlobalSearchPluginSetup, GlobalSearchPluginStart } from './types'; + +export const plugin: PluginInitializer< + GlobalSearchPluginSetup, + GlobalSearchPluginStart, + GlobalSearchPluginSetupDeps, + GlobalSearchPluginStartDeps +> = (context) => new GlobalSearchPlugin(context); + +export { config } from './config'; + +export { + GlobalSearchBatchedResults, + GlobalSearchProviderFindOptions, + GlobalSearchProviderResult, + GlobalSearchProviderResultUrl, + GlobalSearchResult, +} from '../common/types'; +export { + GlobalSearchFindOptions, + GlobalSearchProviderContext, + GlobalSearchPluginStart, + GlobalSearchPluginSetup, + GlobalSearchResultProvider, + RouteHandlerGlobalSearchContext, +} from './types'; diff --git a/x-pack/plugins/global_search/server/mocks.ts b/x-pack/plugins/global_search/server/mocks.ts new file mode 100644 index 0000000000000..8a189a5701708 --- /dev/null +++ b/x-pack/plugins/global_search/server/mocks.ts @@ -0,0 +1,45 @@ +/* + * 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 { of } from 'rxjs'; +import { + GlobalSearchPluginSetup, + GlobalSearchPluginStart, + RouteHandlerGlobalSearchContext, +} from './types'; +import { searchServiceMock } from './services/search_service.mock'; + +const createSetupMock = (): jest.Mocked => { + const searchMock = searchServiceMock.createSetupContract(); + + return { + registerResultProvider: searchMock.registerResultProvider, + }; +}; + +const createStartMock = (): jest.Mocked => { + const searchMock = searchServiceMock.createStartContract(); + + return { + find: searchMock.find, + }; +}; + +const createRouteHandlerContextMock = (): jest.Mocked => { + const contextMock = { + find: jest.fn(), + }; + + contextMock.find.mockReturnValue(of([])); + + return contextMock; +}; + +export const globalSearchPluginMock = { + createSetupContract: createSetupMock, + createStartContract: createStartMock, + createRouteHandlerContext: createRouteHandlerContextMock, +}; diff --git a/x-pack/plugins/global_search/server/plugin.test.mocks.ts b/x-pack/plugins/global_search/server/plugin.test.mocks.ts new file mode 100644 index 0000000000000..1223b1ec20389 --- /dev/null +++ b/x-pack/plugins/global_search/server/plugin.test.mocks.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const registerRoutesMock = jest.fn(); +jest.doMock('./routes', () => ({ + registerRoutes: registerRoutesMock, +})); diff --git a/x-pack/plugins/global_search/server/plugin.test.ts b/x-pack/plugins/global_search/server/plugin.test.ts new file mode 100644 index 0000000000000..e654dbfdc158a --- /dev/null +++ b/x-pack/plugins/global_search/server/plugin.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { registerRoutesMock } from './plugin.test.mocks'; + +import { coreMock } from '../../../../src/core/server/mocks'; +import { GlobalSearchPlugin } from './plugin'; + +describe('GlobalSearchPlugin', () => { + let plugin: GlobalSearchPlugin; + + beforeEach(() => { + plugin = new GlobalSearchPlugin(coreMock.createPluginInitializerContext()); + }); + + it('registers routes during `setup`', async () => { + await plugin.setup(coreMock.createSetup()); + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + }); + + it('registers the globalSearch route handler context', async () => { + const coreSetup = coreMock.createSetup(); + await plugin.setup(coreSetup); + expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1); + expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledWith( + 'globalSearch', + expect.any(Function) + ); + }); +}); diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts new file mode 100644 index 0000000000000..87e7f96b34c0c --- /dev/null +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -0,0 +1,86 @@ +/* + * 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 { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { LicensingPluginStart } from '../../licensing/server'; +import { LicenseChecker, ILicenseChecker } from '../common/license_checker'; +import { SearchService, SearchServiceStart } from './services'; +import { registerRoutes } from './routes'; +import { + GlobalSearchPluginSetup, + GlobalSearchPluginStart, + RouteHandlerGlobalSearchContext, +} from './types'; +import { GlobalSearchConfigType } from './config'; + +declare module 'src/core/server' { + interface RequestHandlerContext { + globalSearch?: RouteHandlerGlobalSearchContext; + } +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface GlobalSearchPluginSetupDeps {} +export interface GlobalSearchPluginStartDeps { + licensing: LicensingPluginStart; +} + +export class GlobalSearchPlugin + implements + Plugin< + GlobalSearchPluginSetup, + GlobalSearchPluginStart, + GlobalSearchPluginSetupDeps, + GlobalSearchPluginStartDeps + > { + private readonly config$: Observable; + private readonly searchService = new SearchService(); + private searchServiceStart?: SearchServiceStart; + private licenseChecker?: ILicenseChecker; + + constructor(context: PluginInitializerContext) { + this.config$ = context.config.create(); + } + + public async setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { + const config = await this.config$.pipe(take(1)).toPromise(); + const { registerResultProvider } = this.searchService.setup({ + basePath: core.http.basePath, + config, + }); + + registerRoutes(core.http.createRouter()); + + core.http.registerRouteHandlerContext('globalSearch', (_, req) => { + return { + find: (term, options) => this.searchServiceStart!.find(term, options, req), + }; + }); + + return { + registerResultProvider, + }; + } + + public start(core: CoreStart, { licensing }: GlobalSearchPluginStartDeps) { + this.licenseChecker = new LicenseChecker(licensing.license$); + this.searchServiceStart = this.searchService.start({ + core, + licenseChecker: this.licenseChecker, + }); + return { + find: this.searchServiceStart.find, + }; + } + + public stop() { + if (this.licenseChecker) { + this.licenseChecker.clean(); + } + } +} diff --git a/x-pack/plugins/global_search/server/routes/find.ts b/x-pack/plugins/global_search/server/routes/find.ts new file mode 100644 index 0000000000000..a9063abda0e3e --- /dev/null +++ b/x-pack/plugins/global_search/server/routes/find.ts @@ -0,0 +1,50 @@ +/* + * 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 { reduce, map } from 'rxjs/operators'; +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { GlobalSearchFindError } from '../../common/errors'; + +export const registerInternalFindRoute = (router: IRouter) => { + router.post( + { + path: '/internal/global_search/find', + validate: { + body: schema.object({ + term: schema.string(), + options: schema.maybe( + schema.object({ + preference: schema.maybe(schema.string()), + }) + ), + }), + }, + }, + async (ctx, req, res) => { + const { term, options } = req.body; + try { + const allResults = await ctx + .globalSearch!.find(term, { ...options, aborted$: req.events.aborted$ }) + .pipe( + map((batch) => batch.results), + reduce((acc, results) => [...acc, ...results]) + ) + .toPromise(); + return res.ok({ + body: { + results: allResults, + }, + }); + } catch (e) { + if (e instanceof GlobalSearchFindError && e.type === 'invalid-license') { + return res.forbidden({ body: e.message }); + } + throw e; + } + } + ); +}; diff --git a/x-pack/plugins/global_search/server/routes/index.test.ts b/x-pack/plugins/global_search/server/routes/index.test.ts new file mode 100644 index 0000000000000..64675bc13cb1c --- /dev/null +++ b/x-pack/plugins/global_search/server/routes/index.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { httpServiceMock } from '../../../../../src/core/server/mocks'; +import { registerRoutes } from './index'; + +describe('registerRoutes', () => { + it('foo', () => { + const router = httpServiceMock.createRouter(); + + registerRoutes(router); + + expect(router.post).toHaveBeenCalledTimes(1); + + expect(router.post).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/internal/global_search/find', + }), + expect.any(Function) + ); + + expect(router.get).toHaveBeenCalledTimes(0); + expect(router.delete).toHaveBeenCalledTimes(0); + expect(router.put).toHaveBeenCalledTimes(0); + }); +}); diff --git a/x-pack/plugins/global_search/server/routes/index.ts b/x-pack/plugins/global_search/server/routes/index.ts new file mode 100644 index 0000000000000..7840b95614993 --- /dev/null +++ b/x-pack/plugins/global_search/server/routes/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { registerInternalFindRoute } from './find'; + +export const registerRoutes = (router: IRouter) => { + registerInternalFindRoute(router); +}; diff --git a/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts new file mode 100644 index 0000000000000..878e4ac896b96 --- /dev/null +++ b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { of, throwError } from 'rxjs'; +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { setupServer } from '../../../../../../src/core/server/test_utils'; +import { GlobalSearchResult, GlobalSearchBatchedResults } from '../../../common/types'; +import { GlobalSearchFindError } from '../../../common/errors'; +import { globalSearchPluginMock } from '../../mocks'; +import { registerInternalFindRoute } from '../find'; + +type setupServerReturn = UnwrapPromise>; +const pluginId = Symbol('globalSearch'); + +const createResult = (id: string): GlobalSearchResult => ({ + id, + title: id, + type: 'test', + url: `/app/test/${id}`, + score: 42, +}); + +const createBatch = (...ids: string[]): GlobalSearchBatchedResults => ({ + results: ids.map(createResult), +}); + +const expectedResults = (...ids: string[]) => ids.map((id) => expect.objectContaining({ id })); + +describe('POST /internal/global_search/find', () => { + let server: setupServerReturn['server']; + let httpSetup: setupServerReturn['httpSetup']; + let globalSearchHandlerContext: ReturnType; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer(pluginId)); + + globalSearchHandlerContext = globalSearchPluginMock.createRouteHandlerContext(); + httpSetup.registerRouteHandlerContext( + pluginId, + 'globalSearch', + () => globalSearchHandlerContext + ); + + const router = httpSetup.createRouter('/'); + + registerInternalFindRoute(router); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('calls the handler context with correct parameters', async () => { + await supertest(httpSetup.server.listener) + .post('/internal/global_search/find') + .send({ + term: 'search', + options: { + preference: 'custom-pref', + }, + }) + .expect(200); + + expect(globalSearchHandlerContext.find).toHaveBeenCalledTimes(1); + expect(globalSearchHandlerContext.find).toHaveBeenCalledWith('search', { + preference: 'custom-pref', + aborted$: expect.any(Object), + }); + }); + + it('returns all the results returned from the service', async () => { + globalSearchHandlerContext.find.mockReturnValue( + of(createBatch('1', '2'), createBatch('3', '4')) + ); + + const response = await supertest(httpSetup.server.listener) + .post('/internal/global_search/find') + .send({ + term: 'search', + }) + .expect(200); + + expect(response.body).toEqual({ + results: expectedResults('1', '2', '3', '4'), + }); + }); + + it('returns a 403 when the observable throws an invalid-license error', async () => { + globalSearchHandlerContext.find.mockReturnValue( + throwError(GlobalSearchFindError.invalidLicense('invalid-license-message')) + ); + + const response = await supertest(httpSetup.server.listener) + .post('/internal/global_search/find') + .send({ + term: 'search', + }) + .expect(403); + + expect(response.body).toEqual( + expect.objectContaining({ + message: 'invalid-license-message', + statusCode: 403, + }) + ); + }); + + it('returns the default error when the observable throws any other error', async () => { + globalSearchHandlerContext.find.mockReturnValue(throwError('any-error')); + + const response = await supertest(httpSetup.server.listener) + .post('/internal/global_search/find') + .send({ + term: 'search', + }) + .expect(500); + + expect(response.body).toEqual( + expect.objectContaining({ + message: 'An internal server error occurred.', + statusCode: 500, + }) + ); + }); +}); diff --git a/x-pack/plugins/global_search/server/services/context.test.ts b/x-pack/plugins/global_search/server/services/context.test.ts new file mode 100644 index 0000000000000..397a1ea170349 --- /dev/null +++ b/x-pack/plugins/global_search/server/services/context.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServerMock, coreMock } from '../../../../../src/core/server/mocks'; +import { getContextFactory } from './context'; + +describe('getContextFactory', () => { + it('returns a GlobalSearchProviderContext bound to the request', () => { + const coreStart = coreMock.createStart(); + const request = httpServerMock.createKibanaRequest(); + + const factory = getContextFactory(coreStart); + const context = factory(request); + + expect(coreStart.savedObjects.getScopedClient).toHaveBeenCalledTimes(1); + expect(coreStart.savedObjects.getScopedClient).toHaveBeenCalledWith(request); + + expect(coreStart.savedObjects.getTypeRegistry).toHaveBeenCalledTimes(1); + + expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledTimes(1); + expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledWith(request); + + const soClient = coreStart.savedObjects.getScopedClient.mock.results[0].value; + expect(coreStart.uiSettings.asScopedToClient).toHaveBeenCalledTimes(1); + expect(coreStart.uiSettings.asScopedToClient).toHaveBeenCalledWith(soClient); + + expect(context).toEqual({ + core: { + savedObjects: expect.any(Object), + elasticsearch: expect.any(Object), + uiSettings: expect.any(Object), + }, + }); + }); +}); diff --git a/x-pack/plugins/global_search/server/services/context.ts b/x-pack/plugins/global_search/server/services/context.ts new file mode 100644 index 0000000000000..b15deccaae018 --- /dev/null +++ b/x-pack/plugins/global_search/server/services/context.ts @@ -0,0 +1,35 @@ +/* + * 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 { CoreStart, KibanaRequest } from 'src/core/server'; +import { GlobalSearchProviderContext } from '../types'; + +export type GlobalSearchContextFactory = (request: KibanaRequest) => GlobalSearchProviderContext; + +/** + * {@link GlobalSearchProviderContext | context} factory + */ +export const getContextFactory = (coreStart: CoreStart) => ( + request: KibanaRequest +): GlobalSearchProviderContext => { + const soClient = coreStart.savedObjects.getScopedClient(request); + return { + core: { + savedObjects: { + client: soClient, + typeRegistry: coreStart.savedObjects.getTypeRegistry(), + }, + elasticsearch: { + legacy: { + client: coreStart.elasticsearch.legacy.client.asScoped(request), + }, + }, + uiSettings: { + client: coreStart.uiSettings.asScopedToClient(soClient), + }, + }, + }; +}; diff --git a/x-pack/plugins/global_search/server/services/index.ts b/x-pack/plugins/global_search/server/services/index.ts new file mode 100644 index 0000000000000..cee5b24d2f588 --- /dev/null +++ b/x-pack/plugins/global_search/server/services/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { SearchService, SearchServiceSetup, SearchServiceStart } from './search_service'; diff --git a/x-pack/plugins/global_search/server/services/search_service.mock.ts b/x-pack/plugins/global_search/server/services/search_service.mock.ts new file mode 100644 index 0000000000000..eca69148288b9 --- /dev/null +++ b/x-pack/plugins/global_search/server/services/search_service.mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchServiceSetup, SearchServiceStart } from './search_service'; +import { of } from 'rxjs'; + +const createSetupMock = (): jest.Mocked => { + return { + registerResultProvider: jest.fn(), + }; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + find: jest.fn(), + }; + mock.find.mockReturnValue(of({ results: [] })); + + return mock; +}; + +export const searchServiceMock = { + createSetupContract: createSetupMock, + createStartContract: createStartMock, +}; diff --git a/x-pack/plugins/global_search/server/services/search_service.test.ts b/x-pack/plugins/global_search/server/services/search_service.test.ts new file mode 100644 index 0000000000000..fd705b4286680 --- /dev/null +++ b/x-pack/plugins/global_search/server/services/search_service.test.ts @@ -0,0 +1,323 @@ +/* + * 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 { Observable, of } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; +import { duration } from 'moment'; +import { httpServiceMock, httpServerMock, coreMock } from '../../../../../src/core/server/mocks'; +import { licenseCheckerMock } from '../../common/license_checker.mock'; +import { GlobalSearchProviderResult } from '../../common/types'; +import { GlobalSearchFindError } from '../../common/errors'; +import { GlobalSearchConfigType } from '../config'; +import { GlobalSearchResultProvider } from '../types'; +import { SearchService } from './search_service'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + +describe('SearchService', () => { + let service: SearchService; + let basePath: ReturnType; + let coreStart: ReturnType; + let licenseChecker: ReturnType; + let request: ReturnType; + + const createConfig = (timeoutMs: number = 30000): GlobalSearchConfigType => { + return { + search_timeout: duration(timeoutMs), + }; + }; + + const createProvider = ( + id: string, + source: Observable = of([]) + ): jest.Mocked => ({ + id, + find: jest.fn().mockImplementation((term, options, context) => source), + }); + + const expectedResult = (id: string) => expect.objectContaining({ id }); + + const expectedBatch = (...ids: string[]) => ({ + results: ids.map((id) => expectedResult(id)), + }); + + const result = ( + id: string, + parts: Partial = {} + ): GlobalSearchProviderResult => ({ + title: id, + type: 'test', + url: '/foo/bar', + score: 100, + ...parts, + id, + }); + + beforeEach(() => { + service = new SearchService(); + basePath = httpServiceMock.createBasePath(); + basePath.prepend.mockImplementation((path) => `/base-path${path}`); + coreStart = coreMock.createStart(); + licenseChecker = licenseCheckerMock.create(); + }); + + describe('#setup()', () => { + describe('#registerResultProvider()', () => { + it('throws when trying to register the same provider twice', () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + const provider = createProvider('A'); + registerResultProvider(provider); + expect(() => { + registerResultProvider(provider); + }).toThrowErrorMatchingInlineSnapshot(`"trying to register duplicate provider: A"`); + }); + }); + }); + + describe('#start()', () => { + describe('#find()', () => { + it('calls the provider with the correct parameters', () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + const provider = createProvider('A'); + registerResultProvider(provider); + + const { find } = service.start({ core: coreStart, licenseChecker }); + find('foobar', { preference: 'pref' }, request); + + expect(provider.find).toHaveBeenCalledTimes(1); + expect(provider.find).toHaveBeenCalledWith( + 'foobar', + expect.objectContaining({ preference: 'pref' }), + expect.objectContaining({ core: expect.any(Object) }) + ); + }); + + it('return the results from the provider', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('a-b-|', { + a: [result('1')], + b: [result('2')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find('foo', {}, request); + + expectObservable(results).toBe('a-b-|', { + a: expectedBatch('1'), + b: expectedBatch('2'), + }); + }); + }); + + it('handles multiple providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider( + 'A', + hot('a---d-|', { + a: [result('A1'), result('A2')], + d: [result('A3')], + }) + ) + ); + registerResultProvider( + createProvider( + 'B', + hot('-b-c| ', { + b: [result('B1')], + c: [result('B2'), result('B3')], + }) + ) + ); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find('foo', {}, request); + + expectObservable(results).toBe('ab-cd-|', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('B2', 'B3'), + d: expectedBatch('A3'), + }); + }); + }); + + it('handles the `aborted$` option', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('--a---(b|)', { + a: [result('1')], + b: [result('2')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const aborted$ = hot('----a--|', { a: undefined }); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find('foo', { aborted$ }, request); + + expectObservable(results).toBe('--a-|', { + a: expectedBatch('1'), + }); + }); + }); + + it('respects the timeout duration', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(100), + basePath, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('a 24ms b 100ms (c|)', { + a: [result('1')], + b: [result('2')], + c: [result('3')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find('foo', {}, request); + + expectObservable(results).toBe('a 24ms b 74ms |', { + a: expectedBatch('1'), + b: expectedBatch('2'), + }); + }); + }); + + it('only returns a given maximum number of results per provider', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(100), + basePath, + maxProviderResults: 2, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider( + 'A', + hot('a---d-|', { + a: [result('A1'), result('A2')], + d: [result('A3')], + }) + ) + ); + registerResultProvider( + createProvider( + 'B', + hot('-b-c| ', { + b: [result('B1')], + c: [result('B2'), result('B3')], + }) + ) + ); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find('foo', {}, request); + + expectObservable(results).toBe('ab-(c|)', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('B2'), + }); + }); + }); + + it('process the results before returning them', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + const resultA = result('A', { + type: 'application', + icon: 'appIcon', + score: 42, + title: 'foo', + url: '/foo/bar', + }); + const resultB = result('B', { + type: 'dashboard', + score: 69, + title: 'bar', + url: { path: '/foo', prependBasePath: false }, + }); + + const provider = createProvider('A', of([resultA, resultB])); + registerResultProvider(provider); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const batch = await find('foo', {}, request).pipe(take(1)).toPromise(); + + expect(batch.results).toHaveLength(2); + expect(batch.results[0]).toEqual({ + ...resultA, + url: '/base-path/foo/bar', + }); + expect(batch.results[1]).toEqual({ + ...resultB, + url: '/foo', + }); + }); + + it('emits an error when the license is invalid', async () => { + licenseChecker.getState.mockReturnValue({ valid: false, message: 'expired' }); + + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + const providerResults = hot('a-b-|', { + a: [result('1')], + b: [result('2')], + }); + registerResultProvider(createProvider('A', providerResults)); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find('foo', {}, request); + + expectObservable(results).toBe( + '#', + {}, + GlobalSearchFindError.invalidLicense( + 'GlobalSearch API is disabled because of invalid license state: expired' + ) + ); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/global_search/server/services/search_service.ts b/x-pack/plugins/global_search/server/services/search_service.ts new file mode 100644 index 0000000000000..12eada2a1385e --- /dev/null +++ b/x-pack/plugins/global_search/server/services/search_service.ts @@ -0,0 +1,162 @@ +/* + * 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 { Observable, timer, merge, throwError } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import { KibanaRequest, CoreStart, IBasePath } from 'src/core/server'; +import { GlobalSearchProviderResult, GlobalSearchBatchedResults } from '../../common/types'; +import { GlobalSearchFindError } from '../../common/errors'; +import { takeInArray } from '../../common/operators'; +import { ILicenseChecker } from '../../common/license_checker'; + +import { processProviderResult } from '../../common/process_result'; +import { GlobalSearchConfigType } from '../config'; +import { getContextFactory, GlobalSearchContextFactory } from './context'; +import { GlobalSearchResultProvider, GlobalSearchFindOptions } from '../types'; + +/** @public */ +export interface SearchServiceSetup { + /** + * Register a result provider to be used by the search service. + * + * @example + * ```ts + * setupDeps.globalSearch.registerResultProvider({ + * id: 'my_provider', + * find: (term, options, context) => { + * const resultPromise = myService.search(term, options, context.core.savedObjects.client); + * return from(resultPromise).pipe(takeUntil(options.aborted$); + * }, + * }); + * ``` + */ + registerResultProvider(provider: GlobalSearchResultProvider): void; +} + +/** @public */ +export interface SearchServiceStart { + /** + * Perform a search for given `term` and {@link GlobalSearchFindOptions | options}. + * + * @example + * ```ts + * startDeps.globalSearch.find('some term').subscribe({ + * next: ({ results }) => { + * addNewResultsToList(results); + * }, + * error: () => {}, + * complete: () => { + * showAsyncSearchIndicator(false); + * } + * }); + * ``` + * + * @remarks + * - Emissions from the resulting observable will only contains **new** results. It is the consumer + * responsibility to aggregate the emission and sort the results if required. + * - Results from the client-side registered providers will not available when performing a search + * from the server-side `find` API. + */ + find( + term: string, + options: GlobalSearchFindOptions, + request: KibanaRequest + ): Observable; +} + +interface SetupDeps { + basePath: IBasePath; + config: GlobalSearchConfigType; + maxProviderResults?: number; +} + +interface StartDeps { + core: CoreStart; + licenseChecker: ILicenseChecker; +} + +const defaultMaxProviderResults = 20; +const mapToUndefined = () => undefined; + +/** @internal */ +export class SearchService { + private readonly providers = new Map(); + private basePath?: IBasePath; + private config?: GlobalSearchConfigType; + private contextFactory?: GlobalSearchContextFactory; + private licenseChecker?: ILicenseChecker; + private maxProviderResults = defaultMaxProviderResults; + + setup({ + basePath, + config, + maxProviderResults = defaultMaxProviderResults, + }: SetupDeps): SearchServiceSetup { + this.basePath = basePath; + this.config = config; + this.maxProviderResults = maxProviderResults; + + return { + registerResultProvider: (provider) => { + if (this.providers.has(provider.id)) { + throw new Error(`trying to register duplicate provider: ${provider.id}`); + } + this.providers.set(provider.id, provider); + }, + }; + } + + start({ core, licenseChecker }: StartDeps): SearchServiceStart { + this.licenseChecker = licenseChecker; + this.contextFactory = getContextFactory(core); + return { + find: (term, options, request) => this.performFind(term, options, request), + }; + } + + private performFind(term: string, options: GlobalSearchFindOptions, request: KibanaRequest) { + const licenseState = this.licenseChecker!.getState(); + if (!licenseState.valid) { + return throwError( + GlobalSearchFindError.invalidLicense( + i18n.translate('xpack.globalSearch.find.invalidLicenseError', { + defaultMessage: `GlobalSearch API is disabled because of invalid license state: {errorMessage}`, + values: { errorMessage: licenseState.message }, + }) + ) + ); + } + + const context = this.contextFactory!(request); + + const timeout$ = timer(this.config!.search_timeout.asMilliseconds()).pipe(map(mapToUndefined)); + const aborted$ = options.aborted$ ? merge(options.aborted$, timeout$) : timeout$; + const providerOptions = { + ...options, + preference: options.preference ?? 'default', + maxResults: this.maxProviderResults, + aborted$, + }; + + const processResult = (result: GlobalSearchProviderResult) => + processProviderResult(result, this.basePath!); + + const providersResults$ = [...this.providers.values()].map((provider) => + provider.find(term, providerOptions, context).pipe( + takeInArray(this.maxProviderResults), + takeUntil(aborted$), + map((results) => results.map((r) => processResult(r))) + ) + ); + + return merge(...providersResults$).pipe( + map((results) => ({ + results, + })) + ); + } +} diff --git a/x-pack/plugins/global_search/server/types.ts b/x-pack/plugins/global_search/server/types.ts new file mode 100644 index 0000000000000..eca4aff366883 --- /dev/null +++ b/x-pack/plugins/global_search/server/types.ts @@ -0,0 +1,110 @@ +/* + * 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 { Observable } from 'rxjs'; +import { + ISavedObjectTypeRegistry, + IScopedClusterClient, + IUiSettingsClient, + SavedObjectsClientContract, +} from 'src/core/server'; +import { + GlobalSearchBatchedResults, + GlobalSearchProviderFindOptions, + GlobalSearchProviderResult, +} from '../common/types'; +import { SearchServiceSetup, SearchServiceStart } from './services'; + +export type GlobalSearchPluginSetup = Pick; +export type GlobalSearchPluginStart = Pick; + +/** + * globalSearch route handler context. + * + * @public + */ +export interface RouteHandlerGlobalSearchContext { + /** + * See {@link SearchServiceStart.find | the find API} + */ + find(term: string, options: GlobalSearchFindOptions): Observable; +} + +/** + * Context passed to server-side {@GlobalSearchResultProvider | result provider}'s `find` method. + * + * @public + */ +export interface GlobalSearchProviderContext { + core: { + savedObjects: { + client: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + }; + elasticsearch: { + legacy: { + client: IScopedClusterClient; + }; + }; + uiSettings: { + client: IUiSettingsClient; + }; + }; +} + +/** + * Options for the server-side {@link GlobalSearchPluginStart.find | find API} + * + * @public + */ +export interface GlobalSearchFindOptions { + /** + * A custom preference token associated with a search 'session' that should be used to get consistent scoring + * when performing calls to ES. Can also be used as a 'session' token for providers returning data from elsewhere + * than an elasticsearch cluster. + * If not specified, a random token will be generated and used. + */ + preference?: string; + /** + * Optional observable to notify that the associated `find` call should be canceled. + * If/when provided and emitting, no further result emission will be performed and the result observable will be completed. + */ + aborted$?: Observable; +} + +/** + * GlobalSearch result provider, to be registered using the {@link GlobalSearchPluginSetup | global search API} + * + * @public + */ +export interface GlobalSearchResultProvider { + /** + * id of the provider + */ + id: string; + /** + * Method that should return an observable used to emit new results from the provider. + * + * See {@GlobalSearchProviderResult | the result type} for the expected result structure. + * + * @example + * ```ts + * // returning all results in a single batch + * setupDeps.globalSearch.registerResultProvider({ + * id: 'my_provider', + * find: (term, { aborted$, preference, maxResults }, context) => { + * const resultPromise = myService.search(term, { preference, maxResults }, context.core.savedObjects.client); + * return from(resultPromise).pipe(takeUntil(aborted$)); + * }, + * }); + * ``` + */ + find( + term: string, + options: GlobalSearchProviderFindOptions, + context: GlobalSearchProviderContext + ): Observable; +} diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index ffca273d66c71..645e6b520013f 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -7,6 +7,7 @@ import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { LicenseState, verifyApiAccess } from '../lib/license_state'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/server'; export function registerSearchRoute({ router, @@ -41,7 +42,7 @@ export function registerSearchRoute({ response ) => { verifyApiAccess(licenseState); - const includeFrozen = await uiSettings.get('search:includeFrozen'); + const includeFrozen = await uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); try { return response.ok({ body: { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap index d64c8c6239fcd..9441ffd731524 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap @@ -92,6 +92,7 @@ Array [ exports[`extend index management ilm summary extension should return extension when index has lifecycle error 1`] = `
testy @@ -564,6 +565,7 @@ exports[`extend index management ilm summary extension should return extension w exports[`extend index management ilm summary extension should return extension when index has lifecycle policy 1`] = ` testy diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap index 857a63826505e..5edc5a9343fc3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap @@ -91,11 +91,10 @@ exports[`policy table should show empty state when there are not any policies 1`
{ store = indexLifecycleManagementStore(); component = ( - + {}} /> ); store.dispatch(fetchedPolicies(policies)); @@ -90,7 +91,7 @@ describe('policy table', () => { store = indexLifecycleManagementStore(); component = ( - + {}} /> ); const rendered = mountWithIntl(component); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js index b5d9b91e8c412..4fa1838115840 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js @@ -115,6 +115,10 @@ const indexWithLifecycleError = { moment.tz.setDefault('utc'); +const getUrlForApp = (appId, options) => { + return appId + '/' + (options ? options.path : ''); +}; + describe('extend index management', () => { describe('retry lifecycle action extension', () => { test('should return null when no indices have index lifecycle policy', () => { @@ -171,13 +175,17 @@ describe('extend index management', () => { describe('add lifecycle policy action extension', () => { test('should return null when index has index lifecycle policy', () => { - const extension = addLifecyclePolicyActionExtension({ indices: [indexWithLifecyclePolicy] }); + const extension = addLifecyclePolicyActionExtension( + { indices: [indexWithLifecyclePolicy] }, + getUrlForApp + ); expect(extension).toBeNull(); }); test('should return null when more than one index is passed', () => { const extension = addLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy, indexWithoutLifecyclePolicy], + getUrlForApp, }); expect(extension).toBeNull(); }); @@ -185,6 +193,7 @@ describe('extend index management', () => { test('should return extension when one index is passed and it does not have lifecycle policy', () => { const extension = addLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy], + getUrlForApp, }); expect(extension.renderConfirmModal).toBeDefined; const component = extension.renderConfirmModal(jest.fn()); @@ -220,20 +229,20 @@ describe('extend index management', () => { describe('ilm summary extension', () => { test('should render null when index has no index lifecycle policy', () => { - const extension = ilmSummaryExtension(indexWithoutLifecyclePolicy); + const extension = ilmSummaryExtension(indexWithoutLifecyclePolicy, getUrlForApp); const rendered = mountWithIntl(extension); expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should return extension when index has lifecycle policy', () => { - const extension = ilmSummaryExtension(indexWithLifecyclePolicy); + const extension = ilmSummaryExtension(indexWithLifecyclePolicy, getUrlForApp); expect(extension).toBeDefined(); const rendered = mountWithIntl(extension); expect(rendered).toMatchSnapshot(); }); test('should return extension when index has lifecycle error', () => { - const extension = ilmSummaryExtension(indexWithLifecycleError); + const extension = ilmSummaryExtension(indexWithLifecycleError, getUrlForApp); expect(extension).toBeDefined(); const rendered = mountWithIntl(extension); expect(rendered).toMatchSnapshot(); diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts index 59e623934c60d..5c89b917163d8 100644 --- a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts @@ -17,6 +17,4 @@ export const PLUGIN = { }), }; -export const BASE_PATH = '/management/data/index_lifecycle_management/'; - export const API_BASE_PATH = '/api/index_lifecycle_management'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx index 993dced20bbe6..11cd5d181f4ad 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx @@ -5,25 +5,35 @@ */ import React, { useEffect } from 'react'; -import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; +import { Router, Switch, Route, Redirect } from 'react-router-dom'; +import { ScopedHistory, ApplicationStart } from 'kibana/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { BASE_PATH } from '../../common/constants'; import { UIM_APP_LOAD } from './constants'; import { EditPolicy } from './sections/edit_policy'; import { PolicyTable } from './sections/policy_table'; import { trackUiMetric } from './services/ui_metric'; -export const App = () => { +export const App = ({ + history, + navigateToApp, +}: { + history: ScopedHistory; + navigateToApp: ApplicationStart['navigateToApp']; +}) => { useEffect(() => trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD), []); return ( - + - - - + + } + /> + - + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index a7d88d31e58fc..eddbb5528ad84 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -7,17 +7,22 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Provider } from 'react-redux'; -import { I18nStart } from 'kibana/public'; +import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; import { UnmountCallback } from 'src/core/public'; import { App } from './app'; import { indexLifecycleManagementStore } from './store'; -export const renderApp = (element: Element, I18nContext: I18nStart['Context']): UnmountCallback => { +export const renderApp = ( + element: Element, + I18nContext: I18nStart['Context'], + history: ScopedHistory, + navigateToApp: ApplicationStart['navigateToApp'] +): UnmountCallback => { render( - + , element diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js index bfe1bbb04338c..28bc8671f29e2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js @@ -140,6 +140,41 @@ export const MinAgeInput = (props) => { defaultMessage: 'hours from index creation', } ); + + minutesOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel', + { + defaultMessage: 'minutes from index creation', + } + ); + + secondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel', + { + defaultMessage: 'seconds from index creation', + } + ); + + millisecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel', + { + defaultMessage: 'milliseconds from index creation', + } + ); + + microsecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel', + { + defaultMessage: 'microseconds from index creation', + } + ); + + nanosecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel', + { + defaultMessage: 'nanoseconds from index creation', + } + ); } return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js index 94186b7fc79d7..998143929afef 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js @@ -36,7 +36,6 @@ import { } from '../../constants'; import { toasts } from '../../services/notification'; -import { goToPolicyList } from '../../services/navigation'; import { findFirstError } from '../../services/find_errors'; import { LearnMoreLink } from '../components'; import { NodeAttrsDetails } from './components/node_attrs_details'; @@ -100,7 +99,7 @@ export class EditPolicy extends Component { backToPolicyList = () => { this.props.setSelectedPolicy(null); - goToPolicyList(); + this.props.history.push('/policies'); }; submit = async () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js index d9d74becf9e5d..dad259681eb7a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js @@ -36,9 +36,8 @@ import { } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; - +import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; import { getIndexListUri } from '../../../../../../../index_management/public'; -import { BASE_PATH } from '../../../../../../common/constants'; import { UIM_EDIT_CLICK } from '../../../../constants'; import { getPolicyPath } from '../../../../services/navigation'; import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; @@ -181,8 +180,9 @@ export class PolicyTable extends Component { /* eslint-disable-next-line @elastic/eui/href-or-on-click */ trackUiMetric('click', UIM_EDIT_CLICK)} + {...reactRouterNavigate(this.props.history, getPolicyPath(value), () => + trackUiMetric('click', UIM_EDIT_CLICK) + )} > {value} @@ -201,7 +201,7 @@ export class PolicyTable extends Component { renderCreatePolicyButton() { return ( { - window.location.hash = getIndexListUri(`ilm.policy:${policy.name}`); + this.props.navigateToApp('management', { + path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`)}`, + }); }, }); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts index 2d518ebb3015e..72e9d51d8fdeb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BASE_PATH } from '../../../common/constants'; - -export const goToPolicyList = () => { - window.location.hash = `${BASE_PATH}policies`; -}; - export const getPolicyPath = (policyName: string): string => { - return encodeURI(`#${BASE_PATH}policies/edit/${encodeURIComponent(policyName)}`); + return encodeURI(`/policies/edit/${encodeURIComponent(policyName)}`); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js index 789de0f528b1b..03538fad9aa83 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js @@ -270,7 +270,9 @@ export const getLifecycle = (state) => { if (phaseName === PHASE_DELETE) { accum[phaseName].actions = { ...accum[phaseName].actions, - delete: {}, + delete: { + ...accum[phaseName].actions.delete, + }, }; } } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js index 110998a7e9354..0bd313c9a9f8d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js @@ -23,7 +23,6 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; -import { BASE_PATH } from '../../../common/constants'; import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; @@ -216,7 +215,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { } render() { const { policies } = this.state; - const { indexName, closeModal } = this.props; + const { indexName, closeModal, getUrlForApp } = this.props; const title = (

- + {value}; + content = ( + + {value} + + ); } else { content = value; } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js index 43f8332f4b6bd..e7afc8f12859c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js @@ -67,7 +67,7 @@ export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices }) }; }; -export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { +export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices, getUrlForApp }) => { if (indices.length !== 1) { return null; } @@ -86,6 +86,7 @@ export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => closeModal={closeModal} index={index} reloadIndices={reloadIndices} + getUrlForApp={getUrlForApp} /> ); }, @@ -123,8 +124,8 @@ export const ilmBannerExtension = (indices) => { }; }; -export const ilmSummaryExtension = (index) => { - return ; +export const ilmSummaryExtension = (index, getUrlForApp) => { + return ; }; export const ilmFilterExtension = (indices) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 8fce57b0e79b0..49856dee47fba 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -42,11 +42,12 @@ export class IndexLifecycleManagementPlugin { id: PLUGIN.ID, title: PLUGIN.TITLE, order: 2, - mount: async ({ element }) => { + mount: async ({ element, history }) => { const [coreStart] = await getStartServices(); const { i18n: { Context: I18nContext }, docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + application: { navigateToApp }, } = coreStart; // Initialize additional services. @@ -55,7 +56,7 @@ export class IndexLifecycleManagementPlugin { ); const { renderApp } = await import('./application'); - return renderApp(element, I18nContext); + return renderApp(element, I18nContext, history, navigateToApp); }, }); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 2eb635e19be48..7bf3f96e2b2ef 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -67,7 +67,7 @@ const warmPhaseSchema = schema.maybe( actions: schema.object({ set_priority: setPrioritySchema, unfollow: unfollowSchema, - read_only: schema.maybe(schema.object({})), // Readonly has no options + readonly: schema.maybe(schema.object({})), // Readonly has no options allocate: allocateSchema, shrink: schema.maybe( schema.object({ @@ -91,6 +91,11 @@ const coldPhaseSchema = schema.maybe( unfollow: unfollowSchema, allocate: allocateSchema, freeze: schema.maybe(schema.object({})), // Freeze has no options + searchable_snapshot: schema.maybe( + schema.object({ + snapshot_repository: schema.string(), + }) + ), }), }) ); @@ -104,7 +109,11 @@ const deletePhaseSchema = schema.maybe( policy: schema.string(), }) ), - delete: schema.maybe(schema.object({})), // Delete has no options + delete: schema.maybe( + schema.object({ + delete_searchable_snapshot: schema.maybe(schema.boolean()), + }) + ), }), }) ); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts deleted file mode 100644 index 57b925b8c6fc1..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts +++ /dev/null @@ -1,201 +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 { ReactWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import { - registerTestBed, - TestBed, - TestBedConfig, - findTestSubject, - nextTick, -} from '../../../../../test_utils'; -import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { BASE_PATH } from '../../../common/constants'; -import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { TemplateDeserialized } from '../../../common'; -import { WithAppDependencies, services } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - store: () => indexManagementStore(services as any), - memoryRouter: { - initialEntries: [`${BASE_PATH}indices?includeHidden=true`], - componentRoutePath: `${BASE_PATH}:section(indices|templates)`, - }, - doMountAsync: true, -}; - -const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); - -export interface IdxMgmtHomeTestBed extends TestBed { - findAction: (action: 'edit' | 'clone' | 'delete') => ReactWrapper; - actions: { - selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void; - selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void; - selectIndexDetailsTab: (tab: 'settings' | 'mappings' | 'stats' | 'edit_settings') => void; - clickReloadButton: () => void; - clickTemplateAction: ( - name: TemplateDeserialized['name'], - action: 'edit' | 'clone' | 'delete' - ) => void; - clickTemplateAt: (index: number) => void; - clickCloseDetailsButton: () => void; - clickActionMenu: (name: TemplateDeserialized['name']) => void; - getIncludeHiddenIndicesToggleStatus: () => boolean; - clickIncludeHiddenIndicesToggle: () => void; - }; -} - -export const setup = async (): Promise => { - const testBed = await initTestBed(); - - /** - * Additional helpers - */ - const findAction = (action: 'edit' | 'clone' | 'delete') => { - const actions = ['edit', 'clone', 'delete']; - const { component } = testBed; - - return component.find('.euiContextMenuItem').at(actions.indexOf(action)); - }; - - /** - * User Actions - */ - - const selectHomeTab = (tab: 'indicesTab' | 'templatesTab') => { - testBed.find(tab).simulate('click'); - }; - - const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => { - const tabs = ['summary', 'settings', 'mappings', 'aliases']; - - testBed.find('templateDetails.tab').at(tabs.indexOf(tab)).simulate('click'); - }; - - const clickReloadButton = () => { - const { find } = testBed; - find('reloadButton').simulate('click'); - }; - - const clickActionMenu = async (templateName: TemplateDeserialized['name']) => { - const { component } = testBed; - - // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions" - // The template name may contain a period (.) so we use bracket syntax for selector - component.find(`div[id="${templateName}-actions"] button`).simulate('click'); - }; - - const clickTemplateAction = ( - templateName: TemplateDeserialized['name'], - action: 'edit' | 'clone' | 'delete' - ) => { - const actions = ['edit', 'clone', 'delete']; - const { component } = testBed; - - clickActionMenu(templateName); - - component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click'); - }; - - const clickTemplateAt = async (index: number) => { - const { component, table, router } = testBed; - const { rows } = table.getMetaData('templateTable'); - const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); - - await act(async () => { - const { href } = templateLink.props(); - router.navigateTo(href!); - await nextTick(); - component.update(); - }); - }; - - const clickCloseDetailsButton = () => { - const { find } = testBed; - - find('closeDetailsButton').simulate('click'); - }; - - const clickIncludeHiddenIndicesToggle = () => { - const { find } = testBed; - find('indexTableIncludeHiddenIndicesToggle').simulate('click'); - }; - - const getIncludeHiddenIndicesToggleStatus = () => { - const { find } = testBed; - const props = find('indexTableIncludeHiddenIndicesToggle').props(); - return Boolean(props['aria-checked']); - }; - - const selectIndexDetailsTab = async ( - tab: 'settings' | 'mappings' | 'stats' | 'edit_settings' - ) => { - const indexDetailsTabs = ['settings', 'mappings', 'stats', 'edit_settings']; - const { find, component } = testBed; - await act(async () => { - find('detailPanelTab').at(indexDetailsTabs.indexOf(tab)).simulate('click'); - }); - component.update(); - }; - - return { - ...testBed, - findAction, - actions: { - selectHomeTab, - selectDetailsTab, - selectIndexDetailsTab, - clickReloadButton, - clickTemplateAction, - clickTemplateAt, - clickCloseDetailsButton, - clickActionMenu, - getIncludeHiddenIndicesToggleStatus, - clickIncludeHiddenIndicesToggle, - }, - }; -}; - -type IdxMgmtTestSubjects = TestSubjects; - -export type TestSubjects = - | 'aliasesTab' - | 'appTitle' - | 'cell' - | 'closeDetailsButton' - | 'createTemplateButton' - | 'deleteSystemTemplateCallOut' - | 'deleteTemplateButton' - | 'deleteTemplatesConfirmation' - | 'documentationLink' - | 'emptyPrompt' - | 'manageTemplateButton' - | 'mappingsTab' - | 'noAliasesCallout' - | 'noMappingsCallout' - | 'noSettingsCallout' - | 'indicesList' - | 'indicesTab' - | 'indexTableIncludeHiddenIndicesToggle' - | 'indexTableIndexNameLink' - | 'reloadButton' - | 'reloadIndicesButton' - | 'row' - | 'sectionError' - | 'sectionLoading' - | 'settingsTab' - | 'summaryTab' - | 'summaryTitle' - | 'systemTemplatesSwitch' - | 'templateDetails' - | 'templateDetails.manageTemplateButton' - | 'templateDetails.sectionLoading' - | 'templateDetails.tab' - | 'templateDetails.title' - | 'templateList' - | 'templateTable' - | 'templatesTab'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index e5bce31ee6de1..da461609f0b83 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -12,7 +12,7 @@ type HttpResponse = Record | any[]; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const setLoadTemplatesResponse = (response: HttpResponse = []) => { - server.respondWith('GET', `${API_BASE_PATH}/templates`, [ + server.respondWith('GET', `${API_BASE_PATH}/index-templates`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), @@ -28,7 +28,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; const setDeleteTemplateResponse = (response: HttpResponse = []) => { - server.respondWith('DELETE', `${API_BASE_PATH}/templates`, [ + server.respondWith('POST', `${API_BASE_PATH}/delete-index-templates`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), @@ -39,7 +39,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.status || 400 : 200; const body = error ? error.body : response; - server.respondWith('GET', `${API_BASE_PATH}/templates/:id`, [ + server.respondWith('GET', `${API_BASE_PATH}/index-templates/:id`, [ status, { 'Content-Type': 'application/json' }, JSON.stringify(body), @@ -50,7 +50,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.body.status || 400 : 200; const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - server.respondWith('PUT', `${API_BASE_PATH}/templates`, [ + server.respondWith('POST', `${API_BASE_PATH}/index-templates`, [ status, { 'Content-Type': 'application/json' }, body, @@ -61,7 +61,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.status || 400 : 200; const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - server.respondWith('PUT', `${API_BASE_PATH}/templates/:name`, [ + server.respondWith('PUT', `${API_BASE_PATH}/index-templates/:name`, [ status, { 'Content-Type': 'application/json' }, body, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index 66021b531919a..8e7755a65af3c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -4,18 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setup as homeSetup } from './home.helpers'; -import { setup as templateCreateSetup } from './template_create.helpers'; -import { setup as templateCloneSetup } from './template_clone.helpers'; -import { setup as templateEditSetup } from './template_edit.helpers'; +import './mocks'; export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../test_utils'; -export { setupEnvironment } from './setup_environment'; +export { setupEnvironment, WithAppDependencies, services } from './setup_environment'; -export const pageHelpers = { - home: { setup: homeSetup }, - templateCreate: { setup: templateCreateSetup }, - templateClone: { setup: templateCloneSetup }, - templateEdit: { setup: templateEditSetup }, -}; +export type TestSubjects = + | 'aliasesTab' + | 'appTitle' + | 'cell' + | 'closeDetailsButton' + | 'createTemplateButton' + | 'createLegacyTemplateButton' + | 'deleteSystemTemplateCallOut' + | 'deleteTemplateButton' + | 'deleteTemplatesConfirmation' + | 'documentationLink' + | 'emptyPrompt' + | 'manageTemplateButton' + | 'mappingsTab' + | 'noAliasesCallout' + | 'noMappingsCallout' + | 'noSettingsCallout' + | 'indicesList' + | 'indicesTab' + | 'indexTableIncludeHiddenIndicesToggle' + | 'indexTableIndexNameLink' + | 'reloadButton' + | 'reloadIndicesButton' + | 'row' + | 'sectionError' + | 'sectionLoading' + | 'settingsTab' + | 'summaryTab' + | 'summaryTitle' + | 'systemTemplatesSwitch' + | 'templateDetails' + | 'templateDetails.manageTemplateButton' + | 'templateDetails.sectionLoading' + | 'templateDetails.tab' + | 'templateDetails.title' + | 'templateList' + | 'templateTable' + | 'templatesTab' + | 'legacyTemplateTable' + | 'viewButton' + | 'filterList.filterItem'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/mocks.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/mocks.ts new file mode 100644 index 0000000000000..6260a987e270a --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/mocks.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +(window as any).Worker = class Worker { + onmessage() {} + postMessage() {} +}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 1eaf7efd17395..0a49191fdb149 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + /* eslint-disable @kbn/eslint/no-restricted-paths */ import React from 'react'; import axios from 'axios'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts deleted file mode 100644 index f5af11330a6d8..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts +++ /dev/null @@ -1,587 +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 } from 'react-dom/test-utils'; -import * as fixtures from '../../test/fixtures'; -import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { IdxMgmtHomeTestBed } from './helpers/home.helpers'; -import { API_BASE_PATH } from '../../common/constants'; - -const { setup } = pageHelpers.home; - -const removeWhiteSpaceOnArrayValues = (array: any[]) => - array.map((value) => { - if (!value.trim) { - return value; - } - return value.trim(); - }); - -jest.mock('ui/new_platform'); - -describe('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); - let testBed: IdxMgmtHomeTestBed; - - afterAll(() => { - server.restore(); - }); - - describe('on component mount', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadIndicesResponse([]); - - testBed = await setup(); - - await act(async () => { - const { component } = testBed; - - await nextTick(); - component.update(); - }); - }); - - test('sets the hash query param base on include hidden indices toggle', () => { - const { actions } = testBed; - expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true); - expect(window.location.hash.includes('includeHidden=true')).toBe(true); - actions.clickIncludeHiddenIndicesToggle(); - expect(window.location.hash.includes('includeHidden=true')).toBe(false); - // Note: this test modifies the shared location.hash state, we put it back the way it was - actions.clickIncludeHiddenIndicesToggle(); - expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true); - expect(window.location.hash.includes('includeHidden=true')).toBe(true); - }); - - test('should set the correct app title', () => { - const { exists, find } = testBed; - expect(exists('appTitle')).toBe(true); - expect(find('appTitle').text()).toEqual('Index Management'); - }); - - test('should have a link to the documentation', () => { - const { exists, find } = testBed; - expect(exists('documentationLink')).toBe(true); - expect(find('documentationLink').text()).toBe('Index Management docs'); - }); - - describe('tabs', () => { - test('should have 2 tabs', () => { - const { find } = testBed; - const templatesTab = find('templatesTab'); - const indicesTab = find('indicesTab'); - - expect(indicesTab.length).toBe(1); - expect(indicesTab.text()).toEqual('Indices'); - expect(templatesTab.length).toBe(1); - expect(templatesTab.text()).toEqual('Index Templates'); - }); - - test('should navigate to Index Templates tab', async () => { - const { exists, actions, component } = testBed; - - expect(exists('indicesList')).toBe(true); - expect(exists('templateList')).toBe(false); - - httpRequestsMockHelpers.setLoadTemplatesResponse([]); - - actions.selectHomeTab('templatesTab'); - - await act(async () => { - await nextTick(); - component.update(); - }); - - expect(exists('indicesList')).toBe(false); - expect(exists('templateList')).toBe(true); - }); - }); - - describe('index templates', () => { - describe('when there are no index templates', () => { - beforeEach(async () => { - const { actions, component } = testBed; - - httpRequestsMockHelpers.setLoadTemplatesResponse([]); - - actions.selectHomeTab('templatesTab'); - - await act(async () => { - await nextTick(); - component.update(); - }); - }); - - test('should display an empty prompt', async () => { - const { exists } = testBed; - - expect(exists('sectionLoading')).toBe(false); - expect(exists('emptyPrompt')).toBe(true); - }); - }); - - describe('when there are index templates', () => { - const template1 = fixtures.getTemplate({ - name: `a${getRandomString()}`, - indexPatterns: ['template1Pattern1*', 'template1Pattern2'], - template: { - settings: { - index: { - number_of_shards: '1', - lifecycle: { - name: 'my_ilm_policy', - }, - }, - }, - }, - }); - const template2 = fixtures.getTemplate({ - name: `b${getRandomString()}`, - indexPatterns: ['template2Pattern1*'], - }); - const template3 = fixtures.getTemplate({ - name: `.c${getRandomString()}`, // mock system template - indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'], - }); - - const templates = [template1, template2, template3]; - - beforeEach(async () => { - const { actions, component } = testBed; - - httpRequestsMockHelpers.setLoadTemplatesResponse(templates); - - actions.selectHomeTab('templatesTab'); - - await act(async () => { - await nextTick(); - component.update(); - }); - }); - - test('should list them in the table', async () => { - const { table } = testBed; - - const { tableCellsValues } = table.getMetaData('templateTable'); - - tableCellsValues.forEach((row, i) => { - const template = templates[i]; - const { name, indexPatterns, order, ilmPolicy } = template; - - const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; - const orderFormatted = order ? order.toString() : order; - - expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ - '', - name, - indexPatterns.join(', '), - ilmPolicyName, - orderFormatted, - '', - '', - '', - '', - ]); - }); - }); - - test('should have a button to reload the index templates', async () => { - const { component, exists, actions } = testBed; - const totalRequests = server.requests.length; - - expect(exists('reloadButton')).toBe(true); - - await act(async () => { - actions.clickReloadButton(); - await nextTick(); - component.update(); - }); - - expect(server.requests.length).toBe(totalRequests + 1); - expect(server.requests[server.requests.length - 1].url).toBe( - `${API_BASE_PATH}/templates` - ); - }); - - test('should have a button to create a new template', () => { - const { exists } = testBed; - expect(exists('createTemplateButton')).toBe(true); - }); - - test('should have a switch to view system templates', async () => { - const { table, exists, component, form } = testBed; - const { rows } = table.getMetaData('templateTable'); - - expect(rows.length).toEqual( - templates.filter((template) => !template.name.startsWith('.')).length - ); - - expect(exists('systemTemplatesSwitch')).toBe(true); - - await act(async () => { - form.toggleEuiSwitch('systemTemplatesSwitch'); - await nextTick(); - component.update(); - }); - - const { rows: updatedRows } = table.getMetaData('templateTable'); - expect(updatedRows.length).toEqual(templates.length); - }); - - test('each row should have a link to the template details panel', async () => { - const { find, exists, actions } = testBed; - - await actions.clickTemplateAt(0); - - expect(exists('templateList')).toBe(true); - expect(exists('templateDetails')).toBe(true); - expect(find('templateDetails.title').text()).toBe(template1.name); - }); - - test('template actions column should have an option to delete', () => { - const { actions, findAction } = testBed; - const { name: templateName } = template1; - - actions.clickActionMenu(templateName); - - const deleteAction = findAction('delete'); - - expect(deleteAction.text()).toEqual('Delete'); - }); - - test('template actions column should have an option to clone', () => { - const { actions, findAction } = testBed; - const { name: templateName } = template1; - - actions.clickActionMenu(templateName); - - const cloneAction = findAction('clone'); - - expect(cloneAction.text()).toEqual('Clone'); - }); - - test('template actions column should have an option to edit', () => { - const { actions, findAction } = testBed; - const { name: templateName } = template1; - - actions.clickActionMenu(templateName); - - const editAction = findAction('edit'); - - expect(editAction.text()).toEqual('Edit'); - }); - - describe('delete index template', () => { - test('should show a confirmation when clicking the delete template button', async () => { - const { actions } = testBed; - const { name: templateName } = template1; - - await actions.clickTemplateAction(templateName, 'delete'); - - // We need to read the document "body" as the modal is added there and not inside - // the component DOM tree. - expect( - document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]') - ).not.toBe(null); - - expect( - document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')! - .textContent - ).toContain('Delete template'); - }); - - test('should show a warning message when attempting to delete a system template', async () => { - const { component, form, actions } = testBed; - - await act(async () => { - form.toggleEuiSwitch('systemTemplatesSwitch'); - await nextTick(); - component.update(); - }); - - const { name: systemTemplateName } = template3; - await actions.clickTemplateAction(systemTemplateName, 'delete'); - - expect( - document.body.querySelector('[data-test-subj="deleteSystemTemplateCallOut"]') - ).not.toBe(null); - }); - - test('should send the correct HTTP request to delete an index template', async () => { - const { component, actions, table } = testBed; - const { rows } = table.getMetaData('templateTable'); - - const templateId = rows[0].columns[2].value; - - const { - name: templateName, - _kbnMeta: { formatVersion }, - } = template1; - await actions.clickTemplateAction(templateName, 'delete'); - - const modal = document.body.querySelector( - '[data-test-subj="deleteTemplatesConfirmation"]' - ); - const confirmButton: HTMLButtonElement | null = modal!.querySelector( - '[data-test-subj="confirmModalConfirmButton"]' - ); - - httpRequestsMockHelpers.setDeleteTemplateResponse({ - results: { - successes: [templateId], - errors: [], - }, - }); - - await act(async () => { - confirmButton!.click(); - await nextTick(); - component.update(); - }); - - const latestRequest = server.requests[server.requests.length - 1]; - - expect(latestRequest.method).toBe('POST'); - expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-templates`); - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ - templates: [{ name: template1.name, formatVersion }], - }); - }); - }); - - describe('detail panel', () => { - beforeEach(async () => { - const template = fixtures.getTemplate({ - name: `a${getRandomString()}`, - indexPatterns: ['template1Pattern1*', 'template1Pattern2'], - }); - - httpRequestsMockHelpers.setLoadTemplateResponse(template); - }); - - test('should show details when clicking on a template', async () => { - const { exists, actions } = testBed; - - expect(exists('templateDetails')).toBe(false); - - await actions.clickTemplateAt(0); - - expect(exists('templateDetails')).toBe(true); - }); - - describe('on mount', () => { - beforeEach(async () => { - const { actions } = testBed; - - await actions.clickTemplateAt(0); - }); - - test('should set the correct title', async () => { - const { find } = testBed; - const { name } = template1; - - expect(find('templateDetails.title').text()).toEqual(name); - }); - - it('should have a close button and be able to close flyout', async () => { - const { actions, component, exists } = testBed; - - expect(exists('closeDetailsButton')).toBe(true); - expect(exists('summaryTab')).toBe(true); - - actions.clickCloseDetailsButton(); - - await act(async () => { - await nextTick(); - component.update(); - }); - - expect(exists('summaryTab')).toBe(false); - }); - - it('should have a manage button', async () => { - const { actions, exists } = testBed; - - await actions.clickTemplateAt(0); - - expect(exists('templateDetails.manageTemplateButton')).toBe(true); - }); - }); - - describe('tabs', () => { - test('should have 4 tabs', async () => { - const template = fixtures.getTemplate({ - name: `a${getRandomString()}`, - indexPatterns: ['template1Pattern1*', 'template1Pattern2'], - template: { - settings: { - index: { - number_of_shards: '1', - }, - }, - mappings: { - _source: { - enabled: false, - }, - properties: { - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', - }, - }, - }, - aliases: { - alias1: {}, - }, - }, - }); - - const { find, actions, exists } = testBed; - - httpRequestsMockHelpers.setLoadTemplateResponse(template); - - await actions.clickTemplateAt(0); - - expect(find('templateDetails.tab').length).toBe(4); - expect(find('templateDetails.tab').map((t) => t.text())).toEqual([ - 'Summary', - 'Settings', - 'Mappings', - 'Aliases', - ]); - - // Summary tab should be initial active tab - expect(exists('summaryTab')).toBe(true); - - // Navigate and verify all tabs - actions.selectDetailsTab('settings'); - expect(exists('summaryTab')).toBe(false); - expect(exists('settingsTab')).toBe(true); - - actions.selectDetailsTab('aliases'); - expect(exists('summaryTab')).toBe(false); - expect(exists('settingsTab')).toBe(false); - expect(exists('aliasesTab')).toBe(true); - - actions.selectDetailsTab('mappings'); - expect(exists('summaryTab')).toBe(false); - expect(exists('settingsTab')).toBe(false); - expect(exists('aliasesTab')).toBe(false); - expect(exists('mappingsTab')).toBe(true); - }); - - test('should show an info callout if data is not present', async () => { - const templateWithNoOptionalFields = fixtures.getTemplate({ - name: `a${getRandomString()}`, - indexPatterns: ['template1Pattern1*', 'template1Pattern2'], - }); - - const { actions, find, exists, component } = testBed; - - httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoOptionalFields); - - await actions.clickTemplateAt(0); - - await act(async () => { - await nextTick(); - component.update(); - }); - - expect(find('templateDetails.tab').length).toBe(4); - expect(exists('summaryTab')).toBe(true); - - // Navigate and verify callout message per tab - actions.selectDetailsTab('settings'); - expect(exists('noSettingsCallout')).toBe(true); - - actions.selectDetailsTab('mappings'); - expect(exists('noMappingsCallout')).toBe(true); - - actions.selectDetailsTab('aliases'); - expect(exists('noAliasesCallout')).toBe(true); - }); - }); - - describe('error handling', () => { - it('should render an error message if error fetching template details', async () => { - const { actions, exists } = testBed; - const error = { - status: 404, - error: 'Not found', - message: 'Template not found', - }; - - httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error }); - - await actions.clickTemplateAt(0); - - expect(exists('sectionError')).toBe(true); - // Manage button should not render if error - expect(exists('templateDetails.manageTemplateButton')).toBe(false); - }); - }); - }); - }); - }); - }); - - describe('index detail panel with % character in index name', () => { - const indexName = 'test%'; - beforeEach(async () => { - const index = { - health: 'green', - status: 'open', - primary: 1, - replica: 1, - documents: 10000, - documents_deleted: 100, - size: '156kb', - primary_size: '156kb', - name: indexName, - }; - httpRequestsMockHelpers.setLoadIndicesResponse([index]); - - testBed = await setup(); - const { component, find } = testBed; - - component.update(); - - find('indexTableIndexNameLink').at(0).simulate('click'); - }); - - test('should encode indexName when loading settings in detail panel', async () => { - const { actions } = testBed; - await actions.selectIndexDetailsTab('settings'); - - const latestRequest = server.requests[server.requests.length - 1]; - expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`); - }); - - test('should encode indexName when loading mappings in detail panel', async () => { - const { actions } = testBed; - await actions.selectIndexDetailsTab('mappings'); - - const latestRequest = server.requests[server.requests.length - 1]; - expect(latestRequest.url).toBe(`${API_BASE_PATH}/mapping/${encodeURIComponent(indexName)}`); - }); - - test('should encode indexName when loading stats in detail panel', async () => { - const { actions } = testBed; - await actions.selectIndexDetailsTab('stats'); - - const latestRequest = server.requests[server.requests.length - 1]; - expect(latestRequest.url).toBe(`${API_BASE_PATH}/stats/${encodeURIComponent(indexName)}`); - }); - - test('should encode indexName when editing settings in detail panel', async () => { - const { actions } = testBed; - await actions.selectIndexDetailsTab('edit_settings'); - - const latestRequest = server.requests[server.requests.length - 1]; - expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`); - }); - }); -}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts new file mode 100644 index 0000000000000..c58109364890a --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts @@ -0,0 +1,46 @@ +/* + * 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; +import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { WithAppDependencies, services, TestSubjects } from '../helpers'; + +const testBedConfig: TestBedConfig = { + store: () => indexManagementStore(services as any), + memoryRouter: { + initialEntries: [`/indices?includeHidden=true`], + componentRoutePath: `/:section(indices|templates)`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); + +export interface HomeTestBed extends TestBed { + actions: { + selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void; + }; +} + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + /** + * User Actions + */ + + const selectHomeTab = (tab: 'indicesTab' | 'templatesTab') => { + testBed.find(tab).simulate('click'); + }; + + return { + ...testBed, + actions: { + selectHomeTab, + }, + }; +}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts new file mode 100644 index 0000000000000..a7ac2ebf9bb02 --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts @@ -0,0 +1,87 @@ +/* + * 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 } from 'react-dom/test-utils'; + +import { setupEnvironment, nextTick } from '../helpers'; +import { HomeTestBed, setup } from './home.helpers'; + +/** + * The below import is required to avoid a console error warn from the "brace" package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import { stubWebWorker } from '../../../../../test_utils/stub_web_worker'; +stubWebWorker(); + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: HomeTestBed; + + afterAll(() => { + server.restore(); + }); + + describe('on component mount', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([]); + + testBed = await setup(); + + await act(async () => { + const { component } = testBed; + + await nextTick(); + component.update(); + }); + }); + + test('should set the correct app title', () => { + const { exists, find } = testBed; + expect(exists('appTitle')).toBe(true); + expect(find('appTitle').text()).toEqual('Index Management'); + }); + + test('should have a link to the documentation', () => { + const { exists, find } = testBed; + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Index Management docs'); + }); + + describe('tabs', () => { + test('should have 2 tabs', () => { + const { find } = testBed; + const templatesTab = find('templatesTab'); + const indicesTab = find('indicesTab'); + + expect(indicesTab.length).toBe(1); + expect(indicesTab.text()).toEqual('Indices'); + expect(templatesTab.length).toBe(1); + expect(templatesTab.text()).toEqual('Index Templates'); + }); + + test('should navigate to Index Templates tab', async () => { + const { exists, actions, component } = testBed; + + expect(exists('indicesList')).toBe(true); + expect(exists('templateList')).toBe(false); + + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); + + actions.selectHomeTab('templatesTab'); + + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('indicesList')).toBe(false); + expect(exists('templateList')).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts new file mode 100644 index 0000000000000..98bd3077670a7 --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import { + registerTestBed, + TestBed, + TestBedConfig, + findTestSubject, +} from '../../../../../test_utils'; +// NOTE: We have to use the Home component instead of the TemplateList component because we depend +// upon react router to provide the name of the template to load in the detail panel. +import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { TemplateDeserialized } from '../../../common'; +import { WithAppDependencies, services, TestSubjects } from '../helpers'; + +const testBedConfig: TestBedConfig = { + store: () => indexManagementStore(services as any), + memoryRouter: { + initialEntries: [`/indices`], + componentRoutePath: `/:section(indices|templates)`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); + +export interface IndexTemplatesTabTestBed extends TestBed { + findAction: (action: 'edit' | 'clone' | 'delete') => ReactWrapper; + actions: { + goToTemplatesList: () => void; + selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void; + clickReloadButton: () => void; + clickTemplateAction: ( + name: TemplateDeserialized['name'], + action: 'edit' | 'clone' | 'delete' + ) => void; + clickTemplateAt: (index: number) => void; + clickCloseDetailsButton: () => void; + clickActionMenu: (name: TemplateDeserialized['name']) => void; + toggleViewItem: (view: 'composable' | 'system') => void; + }; +} + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + /** + * Additional helpers + */ + const findAction = (action: 'edit' | 'clone' | 'delete') => { + const actions = ['edit', 'clone', 'delete']; + const { component } = testBed; + + return component.find('.euiContextMenuItem').at(actions.indexOf(action)); + }; + + /** + * User Actions + */ + + const goToTemplatesList = () => { + testBed.find('templatesTab').simulate('click'); + }; + + const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => { + const tabs = ['summary', 'settings', 'mappings', 'aliases']; + + testBed.find('templateDetails.tab').at(tabs.indexOf(tab)).simulate('click'); + }; + + const clickReloadButton = () => { + const { find } = testBed; + find('reloadButton').simulate('click'); + }; + + const clickActionMenu = async (templateName: TemplateDeserialized['name']) => { + const { component } = testBed; + + // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions" + // The template name may contain a period (.) so we use bracket syntax for selector + component.find(`div[id="${templateName}-actions"] button`).simulate('click'); + }; + + const clickTemplateAction = ( + templateName: TemplateDeserialized['name'], + action: 'edit' | 'clone' | 'delete' + ) => { + const actions = ['edit', 'clone', 'delete']; + const { component } = testBed; + + clickActionMenu(templateName); + + component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click'); + }; + + const clickTemplateAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('legacyTemplateTable'); + const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); + + const { href } = templateLink.props(); + await act(async () => { + router.navigateTo(href!); + }); + component.update(); + }; + + const clickCloseDetailsButton = () => { + const { find } = testBed; + + find('closeDetailsButton').simulate('click'); + }; + + const toggleViewItem = (view: 'composable' | 'system') => { + const { find, component } = testBed; + const views = ['composable', 'system']; + + // First open the pop over + act(() => { + find('viewButton').simulate('click'); + }); + component.update(); + + // Then click on a filter item + act(() => { + find('filterList.filterItem').at(views.indexOf(view)).simulate('click'); + }); + component.update(); + }; + + return { + ...testBed, + findAction, + actions: { + goToTemplatesList, + selectDetailsTab, + clickReloadButton, + clickTemplateAction, + clickTemplateAt, + clickCloseDetailsButton, + clickActionMenu, + toggleViewItem, + }, + }; +}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts new file mode 100644 index 0000000000000..8f6a8dddeb195 --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -0,0 +1,497 @@ +/* + * 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 } from 'react-dom/test-utils'; + +import * as fixtures from '../../../test/fixtures'; +import { API_BASE_PATH } from '../../../common/constants'; +import { setupEnvironment, getRandomString } from '../helpers'; + +import { IndexTemplatesTabTestBed, setup } from './index_templates_tab.helpers'; + +const removeWhiteSpaceOnArrayValues = (array: any[]) => + array.map((value) => { + if (typeof value !== 'string') { + return value; + } + + // Convert non breaking spaces ( ) to ordinary space + return value.trim().replace(/\s/g, ' '); + }); + +describe('Index Templates tab', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: IndexTemplatesTabTestBed; + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([]); + + await act(async () => { + testBed = await setup(); + }); + }); + + describe('when there are no index templates', () => { + beforeEach(async () => { + const { actions, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); + + await act(async () => { + actions.goToTemplatesList(); + }); + component.update(); + }); + + test('should display an empty prompt', async () => { + const { exists } = testBed; + + expect(exists('sectionLoading')).toBe(false); + expect(exists('emptyPrompt')).toBe(true); + }); + }); + + describe('when there are index templates', () => { + // Add a default loadIndexTemplate response + httpRequestsMockHelpers.setLoadTemplateResponse(fixtures.getTemplate()); + + const template1 = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + template: { + settings: { + index: { + number_of_shards: '1', + lifecycle: { + name: 'my_ilm_policy', + }, + }, + }, + }, + }); + + const template2 = fixtures.getTemplate({ + name: `b${getRandomString()}`, + indexPatterns: ['template2Pattern1*'], + }); + + const template3 = fixtures.getTemplate({ + name: `.c${getRandomString()}`, // mock system template + indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'], + }); + + const template4 = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template4Pattern1*', 'template4Pattern2'], + template: { + settings: { + index: { + number_of_shards: '1', + lifecycle: { + name: 'my_ilm_policy', + }, + }, + }, + }, + isLegacy: true, + }); + + const template5 = fixtures.getTemplate({ + name: `b${getRandomString()}`, + indexPatterns: ['template5Pattern1*'], + isLegacy: true, + }); + + const template6 = fixtures.getTemplate({ + name: `.c${getRandomString()}`, // mock system template + indexPatterns: ['template6Pattern1*', 'template6Pattern2', 'template6Pattern3'], + isLegacy: true, + }); + + const templates = [template1, template2, template3]; + const legacyTemplates = [template4, template5, template6]; + + beforeEach(async () => { + const { actions, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates, legacyTemplates }); + + await act(async () => { + actions.goToTemplatesList(); + }); + component.update(); + }); + + test('should list them in the table', async () => { + const { table } = testBed; + + const { tableCellsValues } = table.getMetaData('templateTable'); + const { tableCellsValues: legacyTableCellsValues } = table.getMetaData('legacyTemplateTable'); + + // Test composable table content + tableCellsValues.forEach((row, i) => { + const template = templates[i]; + const { name, indexPatterns, priority, ilmPolicy, composedOf } = template; + + const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; + const composedOfString = composedOf ? composedOf.join(',') : ''; + const priorityFormatted = priority ? priority.toString() : ''; + + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + name, + indexPatterns.join(', '), + ilmPolicyName, + composedOfString, + priorityFormatted, + 'M S A', // Mappings Settings Aliases badges + ]); + }); + + // Test legacy table content + legacyTableCellsValues.forEach((row, i) => { + const template = legacyTemplates[i]; + const { name, indexPatterns, order, ilmPolicy } = template; + + const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; + const orderFormatted = order ? order.toString() : order; + + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + '', + name, + indexPatterns.join(', '), + ilmPolicyName, + orderFormatted, + '', + '', + '', + '', + ]); + }); + }); + + test('should have a button to reload the index templates', async () => { + const { exists, actions } = testBed; + const totalRequests = server.requests.length; + + expect(exists('reloadButton')).toBe(true); + + await act(async () => { + actions.clickReloadButton(); + }); + + expect(server.requests.length).toBe(totalRequests + 1); + expect(server.requests[server.requests.length - 1].url).toBe( + `${API_BASE_PATH}/index-templates` + ); + }); + + test('should have a button to create a new template', () => { + const { exists } = testBed; + expect(exists('createLegacyTemplateButton')).toBe(true); + }); + + test('should have a switch to view system templates', async () => { + const { table, exists, actions } = testBed; + const { rows } = table.getMetaData('legacyTemplateTable'); + + expect(rows.length).toEqual( + legacyTemplates.filter((template) => !template.name.startsWith('.')).length + ); + + expect(exists('viewButton')).toBe(true); + + actions.toggleViewItem('system'); + + const { rows: updatedRows } = table.getMetaData('legacyTemplateTable'); + expect(updatedRows.length).toEqual(legacyTemplates.length); + }); + + test('each row should have a link to the template details panel', async () => { + const { find, exists, actions } = testBed; + + await actions.clickTemplateAt(0); + + expect(exists('templateList')).toBe(true); + expect(exists('templateDetails')).toBe(true); + expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name); + }); + + test('template actions column should have an option to delete', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = legacyTemplates; + + actions.clickActionMenu(templateName); + + const deleteAction = findAction('delete'); + + expect(deleteAction.text()).toEqual('Delete'); + }); + + test('template actions column should have an option to clone', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = legacyTemplates; + + actions.clickActionMenu(templateName); + + const cloneAction = findAction('clone'); + + expect(cloneAction.text()).toEqual('Clone'); + }); + + test('template actions column should have an option to edit', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = legacyTemplates; + + actions.clickActionMenu(templateName); + + const editAction = findAction('edit'); + + expect(editAction.text()).toEqual('Edit'); + }); + + describe('delete index template', () => { + test('should show a confirmation when clicking the delete template button', async () => { + const { actions } = testBed; + const [{ name: templateName }] = legacyTemplates; + + await actions.clickTemplateAction(templateName, 'delete'); + + // We need to read the document "body" as the modal is added there and not inside + // the component DOM tree. + expect( + document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]') + ).not.toBe(null); + + expect( + document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')!.textContent + ).toContain('Delete template'); + }); + + test('should show a warning message when attempting to delete a system template', async () => { + const { exists, actions } = testBed; + + actions.toggleViewItem('system'); + + const { name: systemTemplateName } = legacyTemplates[2]; + await actions.clickTemplateAction(systemTemplateName, 'delete'); + + expect(exists('deleteSystemTemplateCallOut')).toBe(true); + }); + + test('should send the correct HTTP request to delete an index template', async () => { + const { actions, table } = testBed; + const { rows } = table.getMetaData('legacyTemplateTable'); + + const templateId = rows[0].columns[2].value; + + const [ + { + name: templateName, + _kbnMeta: { isLegacy }, + }, + ] = legacyTemplates; + await actions.clickTemplateAction(templateName, 'delete'); + + const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]'); + const confirmButton: HTMLButtonElement | null = modal!.querySelector( + '[data-test-subj="confirmModalConfirmButton"]' + ); + + httpRequestsMockHelpers.setDeleteTemplateResponse({ + results: { + successes: [templateId], + errors: [], + }, + }); + + await act(async () => { + confirmButton!.click(); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + expect(latestRequest.method).toBe('POST'); + expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-index-templates`); + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ + templates: [{ name: legacyTemplates[0].name, isLegacy }], + }); + }); + }); + + describe('detail panel', () => { + beforeEach(async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + isLegacy: true, + }); + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + }); + + test('should show details when clicking on a template', async () => { + const { exists, actions } = testBed; + + expect(exists('templateDetails')).toBe(false); + + await actions.clickTemplateAt(0); + + expect(exists('templateDetails')).toBe(true); + }); + + describe('on mount', () => { + beforeEach(async () => { + const { actions } = testBed; + + await actions.clickTemplateAt(0); + }); + + test('should set the correct title', async () => { + const { find } = testBed; + const [{ name }] = legacyTemplates; + + expect(find('templateDetails.title').text()).toEqual(name); + }); + + it('should have a close button and be able to close flyout', async () => { + const { actions, component, exists } = testBed; + + expect(exists('closeDetailsButton')).toBe(true); + expect(exists('summaryTab')).toBe(true); + + await act(async () => { + actions.clickCloseDetailsButton(); + }); + component.update(); + + expect(exists('summaryTab')).toBe(false); + }); + + it('should have a manage button', async () => { + const { actions, exists } = testBed; + + await actions.clickTemplateAt(0); + + expect(exists('templateDetails.manageTemplateButton')).toBe(true); + }); + }); + + describe('tabs', () => { + test('should have 4 tabs', async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + template: { + settings: { + index: { + number_of_shards: '1', + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + aliases: { + alias1: {}, + }, + }, + isLegacy: true, + }); + + const { find, actions, exists } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + + await actions.clickTemplateAt(0); + + expect(find('templateDetails.tab').length).toBe(4); + expect(find('templateDetails.tab').map((t) => t.text())).toEqual([ + 'Summary', + 'Settings', + 'Mappings', + 'Aliases', + ]); + + // Summary tab should be initial active tab + expect(exists('summaryTab')).toBe(true); + + // Navigate and verify all tabs + actions.selectDetailsTab('settings'); + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(true); + + actions.selectDetailsTab('aliases'); + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(false); + expect(exists('aliasesTab')).toBe(true); + + actions.selectDetailsTab('mappings'); + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(false); + expect(exists('aliasesTab')).toBe(false); + expect(exists('mappingsTab')).toBe(true); + }); + + test('should show an info callout if data is not present', async () => { + const templateWithNoOptionalFields = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + isLegacy: true, + }); + + const { actions, find, exists } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoOptionalFields); + + await actions.clickTemplateAt(0); + + expect(find('templateDetails.tab').length).toBe(4); + expect(exists('summaryTab')).toBe(true); + + // Navigate and verify callout message per tab + actions.selectDetailsTab('settings'); + expect(exists('noSettingsCallout')).toBe(true); + + actions.selectDetailsTab('mappings'); + expect(exists('noMappingsCallout')).toBe(true); + + actions.selectDetailsTab('aliases'); + expect(exists('noAliasesCallout')).toBe(true); + }); + }); + + describe('error handling', () => { + it('should render an error message if error fetching template details', async () => { + const { actions, exists } = testBed; + const error = { + status: 404, + error: 'Not found', + message: 'Template not found', + }; + + httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error }); + + await actions.clickTemplateAt(0); + + expect(exists('sectionError')).toBe(true); + // Manage button should not render if error + expect(exists('templateDetails.manageTemplateButton')).toBe(false); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts new file mode 100644 index 0000000000000..e995932dfa00d --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -0,0 +1,70 @@ +/* + * 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 } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; +import { IndexList } from '../../../public/application/sections/home/index_list'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { WithAppDependencies, services, TestSubjects } from '../helpers'; + +const testBedConfig: TestBedConfig = { + store: () => indexManagementStore(services as any), + memoryRouter: { + initialEntries: [`/indices?includeHiddenIndices=true`], + componentRoutePath: `/:section(indices|templates)`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(IndexList), testBedConfig); + +export interface IndicesTestBed extends TestBed { + actions: { + selectIndexDetailsTab: (tab: 'settings' | 'mappings' | 'stats' | 'edit_settings') => void; + getIncludeHiddenIndicesToggleStatus: () => boolean; + clickIncludeHiddenIndicesToggle: () => void; + }; +} + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + /** + * User Actions + */ + + const clickIncludeHiddenIndicesToggle = () => { + const { find } = testBed; + find('indexTableIncludeHiddenIndicesToggle').simulate('click'); + }; + + const getIncludeHiddenIndicesToggleStatus = () => { + const { find } = testBed; + const props = find('indexTableIncludeHiddenIndicesToggle').props(); + return Boolean(props['aria-checked']); + }; + + const selectIndexDetailsTab = async ( + tab: 'settings' | 'mappings' | 'stats' | 'edit_settings' + ) => { + const indexDetailsTabs = ['settings', 'mappings', 'stats', 'edit_settings']; + const { find, component } = testBed; + await act(async () => { + find('detailPanelTab').at(indexDetailsTabs.indexOf(tab)).simulate('click'); + }); + component.update(); + }; + + return { + ...testBed, + actions: { + selectIndexDetailsTab, + getIncludeHiddenIndicesToggleStatus, + clickIncludeHiddenIndicesToggle, + }, + }; +}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts new file mode 100644 index 0000000000000..11c25ffbb590f --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -0,0 +1,111 @@ +/* + * 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 } from 'react-dom/test-utils'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { setupEnvironment, nextTick } from '../helpers'; +import { IndicesTestBed, setup } from './indices_tab.helpers'; + +/** + * The below import is required to avoid a console error warn from the "brace" package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import { stubWebWorker } from '../../../../../test_utils/stub_web_worker'; +stubWebWorker(); + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: IndicesTestBed; + + afterAll(() => { + server.restore(); + }); + + describe('on component mount', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([]); + + testBed = await setup(); + + await act(async () => { + const { component } = testBed; + + await nextTick(); + component.update(); + }); + }); + + test('toggles the include hidden button through URL hash correctly', () => { + const { actions } = testBed; + expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true); + actions.clickIncludeHiddenIndicesToggle(); + expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(false); + // Note: this test modifies the shared location.hash state, we put it back the way it was + actions.clickIncludeHiddenIndicesToggle(); + expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true); + }); + }); + + describe('index detail panel with % character in index name', () => { + const indexName = 'test%'; + beforeEach(async () => { + const index = { + health: 'green', + status: 'open', + primary: 1, + replica: 1, + documents: 10000, + documents_deleted: 100, + size: '156kb', + primary_size: '156kb', + name: indexName, + }; + httpRequestsMockHelpers.setLoadIndicesResponse([index]); + + testBed = await setup(); + const { component, find } = testBed; + + component.update(); + + find('indexTableIndexNameLink').at(0).simulate('click'); + }); + + test('should encode indexName when loading settings in detail panel', async () => { + const { actions } = testBed; + await actions.selectIndexDetailsTab('settings'); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`); + }); + + test('should encode indexName when loading mappings in detail panel', async () => { + const { actions } = testBed; + await actions.selectIndexDetailsTab('mappings'); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.url).toBe(`${API_BASE_PATH}/mapping/${encodeURIComponent(indexName)}`); + }); + + test('should encode indexName when loading stats in detail panel', async () => { + const { actions } = testBed; + await actions.selectIndexDetailsTab('stats'); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.url).toBe(`${API_BASE_PATH}/stats/${encodeURIComponent(indexName)}`); + }); + + test('should encode indexName when editing settings in detail panel', async () => { + const { actions } = testBed; + await actions.selectIndexDetailsTab('edit_settings'); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`); + }); + }); +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts similarity index 100% rename from x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.helpers.ts similarity index 76% rename from x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.helpers.ts index 36498b99ba143..1a58cfa8fb55e 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.helpers.ts @@ -5,16 +5,16 @@ */ import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { TemplateClone } from '../../../public/application/sections/template_clone'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { WithAppDependencies } from '../helpers'; + import { formSetup } from './template_form.helpers'; import { TEMPLATE_NAME } from './constants'; -import { WithAppDependencies } from './setup_environment'; const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}clone_template/${TEMPLATE_NAME}`], - componentRoutePath: `${BASE_PATH}clone_template/:name`, + initialEntries: [`/clone_template/${TEMPLATE_NAME}`], + componentRoutePath: `/clone_template/:name`, }, doMountAsync: true, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx similarity index 88% rename from x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx index fa9d13d1ddd07..6250ef0dc247d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx @@ -3,21 +3,16 @@ * 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 { act } from 'react-dom/test-utils'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { TemplateFormTestBed } from './helpers/template_form.helpers'; -import { getTemplate } from '../../test/fixtures'; -import { - TEMPLATE_NAME, - INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, - MAPPINGS, -} from './helpers/constants'; - -const { setup } = pageHelpers.templateClone; +import { getTemplate } from '../../../test/fixtures'; +import { setupEnvironment, nextTick } from '../helpers'; -jest.mock('ui/new_platform'); +import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, MAPPINGS } from './constants'; +import { setup } from './template_clone.helpers'; +import { TemplateFormTestBed } from './template_form.helpers'; jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), @@ -58,6 +53,7 @@ describe.skip('', () => { template: { mappings: MAPPINGS, }, + isLegacy: true, }); beforeEach(async () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts similarity index 77% rename from x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts index 14a44968a93c3..ab0a7b8567607 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts @@ -5,15 +5,15 @@ */ import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { TemplateCreate } from '../../../public/application/sections/template_create'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { WithAppDependencies } from '../helpers'; + import { formSetup, TestSubjects } from './template_form.helpers'; -import { WithAppDependencies } from './setup_environment'; const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}create_template`], - componentRoutePath: `${BASE_PATH}create_template`, + initialEntries: [`/create_template`], + componentRoutePath: `/create_template`, }, doMountAsync: true, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx similarity index 96% rename from x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 8f464987418c0..50b35fc76721a 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -3,23 +3,22 @@ * 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 { act } from 'react-dom/test-utils'; -import { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { TemplateFormTestBed } from './helpers/template_form.helpers'; +import { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../common'; +import { setupEnvironment, nextTick } from '../helpers'; + import { TEMPLATE_NAME, SETTINGS, MAPPINGS, ALIASES, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, -} from './helpers/constants'; - -const { setup } = pageHelpers.templateCreate; - -jest.mock('ui/new_platform'); +} from './constants'; +import { setup } from './template_create.helpers'; +import { TemplateFormTestBed } from './template_form.helpers'; jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), @@ -345,7 +344,6 @@ describe.skip('', () => { const latestRequest = server.requests[server.requests.length - 1]; const expected = { - isManaged: false, name: TEMPLATE_NAME, indexPatterns: DEFAULT_INDEX_PATTERNS, template: { @@ -367,7 +365,8 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { - formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, + isManaged: false, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.helpers.ts similarity index 77% rename from x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.helpers.ts index af5fa8b79ecad..29ecd84e585ce 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.helpers.ts @@ -5,16 +5,16 @@ */ import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { TemplateEdit } from '../../../public/application/sections/template_edit'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { WithAppDependencies } from '../helpers'; + import { formSetup, TestSubjects } from './template_form.helpers'; import { TEMPLATE_NAME } from './constants'; -import { WithAppDependencies } from './setup_environment'; const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}edit_template/${TEMPLATE_NAME}`], - componentRoutePath: `${BASE_PATH}edit_template/:name`, + initialEntries: [`/edit_template/${TEMPLATE_NAME}`], + componentRoutePath: `/edit_template/:name`, }, doMountAsync: true, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx similarity index 93% rename from x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 0ed369e9b13f7..88067d479f7e7 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -3,13 +3,16 @@ * 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 { act } from 'react-dom/test-utils'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { TemplateFormTestBed } from './helpers/template_form.helpers'; -import * as fixtures from '../../test/fixtures'; -import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './helpers/constants'; +import * as fixtures from '../../../test/fixtures'; +import { setupEnvironment, nextTick } from '../helpers'; + +import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './constants'; +import { setup } from './template_edit.helpers'; +import { TemplateFormTestBed } from './template_form.helpers'; const UPDATED_INDEX_PATTERN = ['updatedIndexPattern']; const UPDATED_MAPPING_TEXT_FIELD_NAME = 'updated_text_datatype'; @@ -22,10 +25,6 @@ const MAPPING = { }, }; -const { setup } = pageHelpers.templateEdit; - -jest.mock('ui/new_platform'); - jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, @@ -63,6 +62,7 @@ describe.skip('', () => { const templateToEdit = fixtures.getTemplate({ name: 'index_template_without_mappings', indexPatterns: ['indexPattern1'], + isLegacy: true, }); beforeEach(async () => { @@ -103,6 +103,7 @@ describe.skip('', () => { template: { mappings: MAPPING, }, + isLegacy: true, }); beforeEach(async () => { @@ -207,9 +208,9 @@ describe.skip('', () => { settings: SETTINGS, aliases: ALIASES, }, - isManaged: false, _kbnMeta: { - formatVersion: templateToEdit._kbnMeta.formatVersion, + isManaged: false, + isLegacy: templateToEdit._kbnMeta.isLegacy, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts similarity index 99% rename from x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts index 21713428c4316..fdf837a914cf1 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils'; import { TemplateDeserialized } from '../../../common'; -import { nextTick } from './index'; +import { nextTick } from '../helpers'; interface MappingField { name: string; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index ffd3cbb83c2ce..8e8c2632a2372 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -37,8 +37,6 @@ import { findTestSubject } from '@elastic/eui/lib/test'; /* eslint-disable @kbn/eslint/no-restricted-paths */ import { notificationServiceMock } from '../../../../../src/core/public/notifications/notifications_service.mock'; -jest.mock('ui/new_platform'); - const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); let server = null; diff --git a/x-pack/plugins/index_management/__mocks__/ace.js b/x-pack/plugins/index_management/__mocks__/ace.js deleted file mode 100644 index 40ce7026eee11..0000000000000 --- a/x-pack/plugins/index_management/__mocks__/ace.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export default { - edit: () => { - return { - navigateFileEnd() {}, - destroy() {}, - acequire() { - return { - setCompleters() {}, - }; - }, - setValue() {}, - setOptions() {}, - setTheme() {}, - setFontSize() {}, - setShowPrintMargin() {}, - getSession() { - return { - setUseWrapMode() {}, - setMode() {}, - setValue() {}, - on() {}, - }; - }, - renderer: { - setShowGutter() {}, - setScrollMargin() {}, - }, - setBehavioursEnabled() {}, - }; - }, - acequire() { - return { - setCompleters() {}, - }; - }, - setCompleters() { - return [{}]; - }, -}; diff --git a/x-pack/plugins/index_management/__mocks__/ui/documentation_links.js b/x-pack/plugins/index_management/__mocks__/ui/documentation_links.js deleted file mode 100644 index 0da03ba9b98ba..0000000000000 --- a/x-pack/plugins/index_management/__mocks__/ui/documentation_links.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const settingsDocumentationLink = 'https://stuff.com/docs'; diff --git a/x-pack/plugins/index_management/__mocks__/ui/notify.js b/x-pack/plugins/index_management/__mocks__/ui/notify.js deleted file mode 100644 index 3d64a99232bc3..0000000000000 --- a/x-pack/plugins/index_management/__mocks__/ui/notify.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const toastNotifications = { - addInfo: () => {}, - addSuccess: () => {}, - addDanger: () => {}, - addWarning: () => {}, - addError: () => {}, -}; - -export function fatalError() {} diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index 966e2e8e64838..526b9fede2a67 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -9,7 +9,7 @@ export { BASE_PATH } from './base_path'; export { API_BASE_PATH } from './api_base_path'; export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters'; export * from './index_statuses'; -export { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './index_templates'; +export { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from './index_templates'; export { UIM_APP_NAME, diff --git a/x-pack/plugins/index_management/common/constants/index_templates.ts b/x-pack/plugins/index_management/common/constants/index_templates.ts index 788e96ee895ed..7696b3832c51e 100644 --- a/x-pack/plugins/index_management/common/constants/index_templates.ts +++ b/x-pack/plugins/index_management/common/constants/index_templates.ts @@ -6,7 +6,7 @@ /** * Up until the end of the 8.x release cycle we need to support both - * V1 and V2 index template formats. This constant keeps track of whether - * we create V1 or V2 index template format in the UI. + * legacy and composable index template formats. This constant keeps track of whether + * we create legacy index template format by default in the UI. */ -export const DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT = 1; +export const CREATE_LEGACY_TEMPLATE_BY_DEFAULT = true; diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts index 459eda7552c85..3792e322ae40b 100644 --- a/x-pack/plugins/index_management/common/index.ts +++ b/x-pack/plugins/index_management/common/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PLUGIN, API_BASE_PATH, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './constants'; +export { PLUGIN, API_BASE_PATH, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from './constants'; export { getTemplateParameter } from './lib'; diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index 33f7fbe45182e..16eb544c56a08 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ export { + deserializeLegacyTemplateList, deserializeTemplateList, - deserializeV1Template, - serializeV1Template, + deserializeLegacyTemplate, + serializeLegacyTemplate, } from './template_serialization'; export { getTemplateParameter } from './utils'; diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 33a83d1e9335b..249881f668d9f 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -5,60 +5,34 @@ */ import { TemplateDeserialized, - TemplateV1Serialized, - TemplateV2Serialized, + LegacyTemplateSerialized, + TemplateSerialized, TemplateListItem, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; -export function serializeV1Template(template: TemplateDeserialized): TemplateV1Serialized { - const { - name, - version, - order, - indexPatterns, - template: { settings, aliases, mappings } = {} as TemplateDeserialized['template'], - } = template; +export function serializeTemplate(templateDeserialized: TemplateDeserialized): TemplateSerialized { + const { version, priority, indexPatterns, template, composedOf } = templateDeserialized; - const serializedTemplate: TemplateV1Serialized = { - name, + return { version, - order, + priority, + template, index_patterns: indexPatterns, - settings, - aliases, - mappings, - }; - - return serializedTemplate; -} - -export function serializeV2Template(template: TemplateDeserialized): TemplateV2Serialized { - const { aliases, mappings, settings, ...templateV1serialized } = serializeV1Template(template); - - return { - ...templateV1serialized, - template: { - aliases, - mappings, - settings, - }, - priority: template.priority, - composed_of: template.composedOf, + composed_of: composedOf, }; } -export function deserializeV2Template( - templateEs: TemplateV2Serialized, +export function deserializeTemplate( + templateEs: TemplateSerialized & { name: string }, managedTemplatePrefix?: string ): TemplateDeserialized { const { name, version, - order, index_patterns: indexPatterns, - template, + template = {}, priority, composed_of: composedOf, } = templateEs; @@ -67,49 +41,92 @@ export function deserializeV2Template( const deserializedTemplate: TemplateDeserialized = { name, version, - order, + priority, indexPatterns: indexPatterns.sort(), template, - ilmPolicy: settings && settings.index && settings.index.lifecycle, - isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), - priority, + ilmPolicy: settings?.index?.lifecycle, composedOf, _kbnMeta: { - formatVersion: 2, + isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), }, }; return deserializedTemplate; } -export function deserializeV1Template( - templateEs: TemplateV1Serialized, +export function deserializeTemplateList( + indexTemplates: Array<{ name: string; index_template: TemplateSerialized }>, + managedTemplatePrefix?: string +): TemplateListItem[] { + return indexTemplates.map(({ name, index_template: templateSerialized }) => { + const { + template: { mappings, settings, aliases }, + ...deserializedTemplate + } = deserializeTemplate({ name, ...templateSerialized }, managedTemplatePrefix); + + return { + ...deserializedTemplate, + hasSettings: hasEntries(settings), + hasAliases: hasEntries(aliases), + hasMappings: hasEntries(mappings), + }; + }); +} + +/** + * ------------------------------------------ + * --------- LEGACY INDEX TEMPLATES --------- + * ------------------------------------------ + */ + +export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyTemplateSerialized { + const { + version, + order, + indexPatterns, + template: { settings, aliases, mappings }, + } = template; + + return { + version, + order, + index_patterns: indexPatterns, + settings, + aliases, + mappings, + }; +} + +export function deserializeLegacyTemplate( + templateEs: LegacyTemplateSerialized & { name: string }, managedTemplatePrefix?: string ): TemplateDeserialized { const { settings, aliases, mappings, ...rest } = templateEs; - const deserializedTemplateV2 = deserializeV2Template( + const deserializedTemplate = deserializeTemplate( { ...rest, template: { aliases, settings, mappings } }, managedTemplatePrefix ); return { - ...deserializedTemplateV2, + ...deserializedTemplate, + order: templateEs.order, _kbnMeta: { - formatVersion: 1, + ...deserializedTemplate._kbnMeta, + isLegacy: true, }, }; } -export function deserializeTemplateList( - indexTemplatesByName: { [key: string]: Omit }, +export function deserializeLegacyTemplateList( + indexTemplatesByName: { [key: string]: LegacyTemplateSerialized }, managedTemplatePrefix?: string ): TemplateListItem[] { return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => { const { template: { mappings, settings, aliases }, ...deserializedTemplate - } = deserializeV1Template({ name, ...templateSerialized }, managedTemplatePrefix); + } = deserializeLegacyTemplate({ name, ...templateSerialized }, managedTemplatePrefix); return { ...deserializedTemplate, diff --git a/x-pack/plugins/index_management/common/lib/utils.test.ts b/x-pack/plugins/index_management/common/lib/utils.test.ts index 221d1b009cede..056101061a82b 100644 --- a/x-pack/plugins/index_management/common/lib/utils.test.ts +++ b/x-pack/plugins/index_management/common/lib/utils.test.ts @@ -3,12 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { TemplateV1Serialized, TemplateV2Serialized } from '../types'; -import { getTemplateVersion } from './utils'; +import { LegacyTemplateSerialized, TemplateSerialized } from '../types'; +import { isLegacyTemplate } from './utils'; describe('utils', () => { - describe('getTemplateVersion', () => { - test('should detect v1 template', () => { + describe('isLegacyTemplate', () => { + test('should detect legacy template', () => { const template = { name: 'my_template', index_patterns: ['logs*'], @@ -16,10 +16,10 @@ describe('utils', () => { properties: {}, }, }; - expect(getTemplateVersion(template as TemplateV1Serialized)).toBe(1); + expect(isLegacyTemplate(template as LegacyTemplateSerialized)).toBe(true); }); - test('should detect v2 template', () => { + test('should detect composable template', () => { const template = { name: 'my_template', index_patterns: ['logs*'], @@ -29,7 +29,7 @@ describe('utils', () => { }, }, }; - expect(getTemplateVersion(template as TemplateV2Serialized)).toBe(2); + expect(isLegacyTemplate(template as TemplateSerialized)).toBe(false); }); }); }); diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts index eee35dc1ab467..5a7db8ef50ab4 100644 --- a/x-pack/plugins/index_management/common/lib/utils.ts +++ b/x-pack/plugins/index_management/common/lib/utils.ts @@ -4,26 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TemplateDeserialized, TemplateV1Serialized, TemplateV2Serialized } from '../types'; +import { TemplateDeserialized, LegacyTemplateSerialized, TemplateSerialized } from '../types'; /** - * Helper to get the format version of an index template. - * v1 will be supported up until 9.x but marked as deprecated from 7.8 - * v2 will be supported from 7.8 + * Helper to know if a template has the legacy format or not + * legacy format will be supported up until 9.x but marked as deprecated from 7.8 + * new (composable) format is supported from 7.8 */ -export const getTemplateVersion = ( - template: TemplateDeserialized | TemplateV1Serialized | TemplateV2Serialized -): 1 | 2 => { - return {}.hasOwnProperty.call(template, 'template') ? 2 : 1; +export const isLegacyTemplate = ( + template: TemplateDeserialized | LegacyTemplateSerialized | TemplateSerialized +): boolean => { + return {}.hasOwnProperty.call(template, 'template') ? false : true; }; export const getTemplateParameter = ( - template: TemplateV1Serialized | TemplateV2Serialized, + template: LegacyTemplateSerialized | TemplateSerialized, setting: 'aliases' | 'settings' | 'mappings' ) => { - const formatVersion = getTemplateVersion(template); - - return formatVersion === 1 - ? (template as TemplateV1Serialized)[setting] - : (template as TemplateV2Serialized).template[setting]; + return isLegacyTemplate(template) + ? (template as LegacyTemplateSerialized)[setting] + : (template as TemplateSerialized).template[setting]; }; diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index c37088982f207..f113aa44d058f 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -8,28 +8,45 @@ import { IndexSettings } from './indices'; import { Aliases } from './aliases'; import { Mappings } from './mappings'; -// Template serialized (from Elasticsearch) -interface TemplateBaseSerialized { - name: string; +/** + * Index template format from Elasticsearch + */ +export interface TemplateSerialized { index_patterns: string[]; + template: { + settings?: IndexSettings; + aliases?: Aliases; + mappings?: Mappings; + }; + composed_of?: string[]; version?: number; - order?: number; -} - -export interface TemplateV1Serialized extends TemplateBaseSerialized { - settings?: IndexSettings; - aliases?: Aliases; - mappings?: Mappings; + priority?: number; } -export interface TemplateV2Serialized extends TemplateBaseSerialized { +/** + * TemplateDeserialized is the format the UI will be working with, + * regardless if we are loading the new format (composable) index template, + * or the legacy one. Serialization is done server side. + */ +export interface TemplateDeserialized { + name: string; + indexPatterns: string[]; template: { settings?: IndexSettings; aliases?: Aliases; mappings?: Mappings; }; + composedOf?: string[]; // Used on composable index template + version?: number; priority?: number; - composed_of?: string[]; + order?: number; // Used on legacy index template + ilmPolicy?: { + name: string; + }; + _kbnMeta: { + isManaged: boolean; + isLegacy?: boolean; + }; } /** @@ -42,42 +59,30 @@ export interface TemplateListItem { indexPatterns: string[]; version?: number; order?: number; + priority?: number; hasSettings: boolean; hasAliases: boolean; hasMappings: boolean; ilmPolicy?: { name: string; }; - isManaged: boolean; _kbnMeta: { - formatVersion: IndexTemplateFormatVersion; + isManaged: boolean; + isLegacy?: boolean; }; } /** - * TemplateDeserialized falls back to index template V2 format - * The UI will only be dealing with this interface, conversion from and to V1 format - * is done server side. + * ------------------------------------------ + * --------- LEGACY INDEX TEMPLATES --------- + * ------------------------------------------ */ -export interface TemplateDeserialized { - name: string; - indexPatterns: string[]; - isManaged: boolean; - template: { - settings?: IndexSettings; - aliases?: Aliases; - mappings?: Mappings; - }; - _kbnMeta: { - formatVersion: IndexTemplateFormatVersion; - }; + +export interface LegacyTemplateSerialized { + index_patterns: string[]; version?: number; - priority?: number; + settings?: IndexSettings; + aliases?: Aliases; + mappings?: Mappings; order?: number; - ilmPolicy?: { - name: string; - }; - composedOf?: string[]; } - -export type IndexTemplateFormatVersion = 1 | 2; diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index 83997dd6ece18..10bbe3ced64da 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -5,8 +5,9 @@ */ import React, { useEffect } from 'react'; -import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; -import { BASE_PATH, UIM_APP_LOAD } from '../../common/constants'; +import { Router, Switch, Route, Redirect } from 'react-router-dom'; +import { ScopedHistory } from 'kibana/public'; +import { UIM_APP_LOAD } from '../../common/constants'; import { IndexManagementHome } from './sections/home'; import { TemplateCreate } from './sections/template_create'; import { TemplateClone } from './sections/template_clone'; @@ -14,24 +15,24 @@ import { TemplateEdit } from './sections/template_edit'; import { useServices } from './app_context'; -export const App = () => { +export const App = ({ history }: { history: ScopedHistory }) => { const { uiMetricService } = useServices(); useEffect(() => uiMetricService.trackMetric('loaded', UIM_APP_LOAD), [uiMetricService]); return ( - + - + ); }; // Export this so we can test it with a different router. export const AppWithoutRouter = () => ( - - - - - + + + + + ); diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 2bb618ad8efce..84938de416941 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -5,6 +5,7 @@ */ import React, { createContext, useContext } from 'react'; +import { ScopedHistory } from 'kibana/public'; import { CoreStart } from '../../../../../src/core/public'; import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; @@ -17,6 +18,7 @@ const AppContext = createContext(undefined); export interface AppDependencies { core: { fatalErrors: CoreStart['fatalErrors']; + getUrlForApp: CoreStart['application']['getUrlForApp']; }; plugins: { usageCollection: UsageCollectionSetup; @@ -27,6 +29,7 @@ export interface AppDependencies { httpService: HttpService; notificationService: NotificationService; }; + history: ScopedHistory; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx index a87412ef92950..06babb0db3bd1 100644 --- a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx @@ -9,7 +9,6 @@ import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } fr import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexTemplateFormatVersion } from '../../../common'; import { deleteTemplates } from '../services/api'; import { notificationService } from '../services/notification'; @@ -17,7 +16,7 @@ export const TemplateDeleteModal = ({ templatesToDelete, callback, }: { - templatesToDelete: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>; + templatesToDelete: Array<{ name: string; isLegacy?: boolean }>; callback: (data?: { hasDeletedTemplates: boolean }) => void; }) => { const [isDeleteConfirmed, setIsDeleteConfirmed] = useState(false); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx index 7b266034bc336..387887239aaf0 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx @@ -23,8 +23,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { serializers } from '../../../../shared_imports'; import { - serializeV1Template, - serializeV2Template, + serializeLegacyTemplate, + serializeTemplate, } from '../../../../../common/lib/template_serialization'; import { TemplateDeserialized, getTemplateParameter } from '../../../../../common'; import { StepProps } from '../types'; @@ -60,16 +60,20 @@ export const StepReview: React.FunctionComponent = ({ template, updat indexPatterns, version, order, - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template!; - const serializedTemplate = - formatVersion === 1 - ? serializeV1Template(stripEmptyFields(template!) as TemplateDeserialized) - : serializeV2Template(stripEmptyFields(template!) as TemplateDeserialized); - - // Name not included in ES request body - delete serializedTemplate.name; + const serializedTemplate = isLegacy + ? serializeLegacyTemplate( + stripEmptyFields(template!, { + types: ['string'], + }) as TemplateDeserialized + ) + : serializeTemplate( + stripEmptyFields(template!, { + types: ['string'], + }) as TemplateDeserialized + ); const serializedMappings = getTemplateParameter(serializedTemplate, 'mappings'); const serializedSettings = getTemplateParameter(serializedTemplate, 'settings'); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 0cdfaae70f151..52e26e6d3e895 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { serializers } from '../../../shared_imports'; -import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common'; +import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common'; import { TemplateSteps } from './template_steps'; import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps'; import { StepProps, DataGetterFunc } from './types'; @@ -51,9 +51,9 @@ export const TemplateForm: React.FunctionComponent = ({ name: '', indexPatterns: [], template: {}, - isManaged: false, _kbnMeta: { - formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isManaged: false, + isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, }, }, onSave, @@ -246,7 +246,9 @@ export const TemplateForm: React.FunctionComponent = ({ iconType="check" onClick={onSave.bind( null, - stripEmptyFields(template.current!) as TemplateDeserialized + stripEmptyFields(template.current!, { + types: ['string'], + }) as TemplateDeserialized )} data-test-subj="submitButton" isLoading={isSaving} diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index 5850cb8d42f1a..8da556cc81fcc 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -24,13 +24,13 @@ export const renderApp = ( const { i18n } = core; const { Context: I18nContext } = i18n; - const { services } = dependencies; + const { services, history } = dependencies; render( - + , diff --git a/x-pack/plugins/index_management/__jest__/lib/__snapshots__/flatten_object.test.js.snap b/x-pack/plugins/index_management/public/application/lib/__snapshots__/flatten_object.test.js.snap similarity index 100% rename from x-pack/plugins/index_management/__jest__/lib/__snapshots__/flatten_object.test.js.snap rename to x-pack/plugins/index_management/public/application/lib/__snapshots__/flatten_object.test.js.snap diff --git a/x-pack/plugins/index_management/__jest__/lib/flatten_object.test.js b/x-pack/plugins/index_management/public/application/lib/flatten_object.test.js similarity index 90% rename from x-pack/plugins/index_management/__jest__/lib/flatten_object.test.js rename to x-pack/plugins/index_management/public/application/lib/flatten_object.test.js index 0d6d5ee796627..222d172d1ff86 100644 --- a/x-pack/plugins/index_management/__jest__/lib/flatten_object.test.js +++ b/x-pack/plugins/index_management/public/application/lib/flatten_object.test.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flattenObject } from '../../public/application/lib/flatten_object'; +import { flattenObject } from './flatten_object'; + describe('flatten_object', () => { test('it flattens an object', () => { const obj = { @@ -17,6 +18,7 @@ describe('flatten_object', () => { }; expect(flattenObject(obj)).toMatchSnapshot(); }); + test('it flattens an object that contains an array in a field', () => { const obj = { foo: { diff --git a/x-pack/plugins/index_management/public/application/lib/index_templates.ts b/x-pack/plugins/index_management/public/application/lib/index_templates.ts index 7129e536287c1..08102ae93cc0e 100644 --- a/x-pack/plugins/index_management/public/application/lib/index_templates.ts +++ b/x-pack/plugins/index_management/public/application/lib/index_templates.ts @@ -6,12 +6,12 @@ import { parse } from 'query-string'; import { Location } from 'history'; -export const getFormatVersionFromQueryparams = (location: Location): 1 | 2 | undefined => { - const { v: version } = parse(location.search.substring(1)); +export const getIsLegacyFromQueryParams = (location: Location): boolean => { + const { legacy } = parse(location.search.substring(1)); - if (!Boolean(version) || typeof version !== 'string') { - return undefined; + if (!Boolean(legacy) || typeof legacy !== 'string') { + return false; } - return +version as 1 | 2; + return legacy === 'true'; }; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index c47b0603dc1c8..e8b6f200fb349 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -30,9 +30,9 @@ export async function mountManagementSection( services: InternalServices, params: ManagementAppMountParams ) { - const { element, setBreadcrumbs } = params; + const { element, setBreadcrumbs, history } = params; const [core] = await coreSetup.getStartServices(); - const { docLinks, fatalErrors } = core; + const { docLinks, fatalErrors, application } = core; breadcrumbService.setup(setBreadcrumbs); documentationService.setup(docLinks); @@ -40,11 +40,13 @@ export async function mountManagementSection( const appDependencies: AppDependencies = { core: { fatalErrors, + getUrlForApp: application.getUrlForApp, }, plugins: { usageCollection, }, services, + history, }; return renderApp(element, { core, dependencies: appDependencies }); diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx index 8e8616d24be20..9d4331d742a25 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx @@ -18,7 +18,6 @@ import { EuiTabs, EuiTitle, } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; import { documentationService } from '../../services/documentation'; import { IndexList } from './index_list'; import { TemplateList } from './template_list'; @@ -53,7 +52,7 @@ export const IndexManagementHome: React.FunctionComponent { - history.push(`${BASE_PATH}${newSection}`); + history.push(`/${newSection}`); }; useEffect(() => { @@ -107,9 +106,13 @@ export const IndexManagementHome: React.FunctionComponent - - - + + + diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js index e49b3c353931e..2fda71035fb58 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js @@ -54,14 +54,14 @@ const getHeaders = () => { }; export class Summary extends React.PureComponent { - getAdditionalContent(extensionsService) { + getAdditionalContent(extensionsService, getUrlForApp) { const { index } = this.props; const extensions = extensionsService.summaries; return extensions.map((summaryExtension, i) => { return ( - {summaryExtension(index)} + {summaryExtension(index, getUrlForApp)} ); }); @@ -103,9 +103,12 @@ export class Summary extends React.PureComponent { render() { return ( - {({ services }) => { + {({ services, core }) => { const { left, right } = this.buildRows(); - const additionalContent = this.getAdditionalContent(services.extensionsService); + const additionalContent = this.getAdditionalContent( + services.extensionsService, + core.getUrlForApp + ); return ( diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index.ts b/x-pack/plugins/index_management/public/application/sections/home/index_list/index.ts index a6d4bfee29d55..cc0e145909e62 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './index_list'; +export { IndexList } from './index_list'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index effd80c39f0d1..1931884cf7306 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -46,7 +46,7 @@ export class IndexActionsContextMenu extends Component { confirmAction = (isActionConfirmed) => { this.setState({ isActionConfirmed }); }; - panels({ services: { extensionsService } }) { + panels({ services: { extensionsService }, core: { getUrlForApp } }) { const { closeIndices, openIndices, @@ -214,6 +214,7 @@ export class IndexActionsContextMenu extends Component { const actionExtensionDefinition = actionExtension({ indices, reloadIndices, + getUrlForApp, }); if (actionExtensionDefinition) { const { diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.js index 4b58ff0de17ee..df5ca7f837d10 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.js @@ -8,15 +8,10 @@ import React from 'react'; import { DetailPanel } from './detail_panel'; import { IndexTable } from './index_table'; -export function IndexList({ - match: { - params: { filter }, - }, - location, -}) { +export function IndexList() { return (

- +
); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js index 44d811f490d9d..9a44a02a44f87 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js @@ -5,13 +5,13 @@ */ import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; import { getDetailPanelIndexName, getPageOfIndices, getPager, getFilter, isDetailPanelOpen, - showHiddenIndices, getSortField, isSortAscending, getIndicesAsArray, @@ -26,7 +26,6 @@ import { pageChanged, pageSizeChanged, sortChanged, - showHiddenIndicesChanged, loadIndices, reloadIndices, toggleChanged, @@ -34,15 +33,14 @@ import { import { IndexTable as PresentationComponent } from './index_table'; -const mapStateToProps = (state) => { +const mapStateToProps = (state, props) => { return { allIndices: getIndicesAsArray(state), isDetailPanelOpen: isDetailPanelOpen(state), detailPanelIndexName: getDetailPanelIndexName(state), - indices: getPageOfIndices(state), - pager: getPager(state), + indices: getPageOfIndices(state, props), + pager: getPager(state, props), filter: getFilter(state), - showHiddenIndices: showHiddenIndices(state), sortField: getSortField(state), isSortAscending: isSortAscending(state), indicesLoading: indicesLoading(state), @@ -65,9 +63,6 @@ const mapDispatchToProps = (dispatch) => { sortChanged: (sortField, isSortAscending) => { dispatch(sortChanged({ sortField, isSortAscending })); }, - showHiddenIndicesChanged: (showHiddenIndices) => { - dispatch(showHiddenIndicesChanged({ showHiddenIndices })); - }, toggleChanged: (toggleName, toggleValue) => { dispatch(toggleChanged({ toggleName, toggleValue })); }, @@ -80,10 +75,12 @@ const mapDispatchToProps = (dispatch) => { loadIndices: () => { dispatch(loadIndices()); }, - reloadIndices: () => { - dispatch(reloadIndices()); + reloadIndices: (indexNames) => { + dispatch(reloadIndices(indexNames)); }, }; }; -export const IndexTable = connect(mapStateToProps, mapDispatchToProps)(PresentationComponent); +export const IndexTable = withRouter( + connect(mapStateToProps, mapDispatchToProps)(PresentationComponent) +); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index 0d005b2864863..f33d486520a29 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -8,7 +8,7 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Route } from 'react-router-dom'; -import { parse } from 'query-string'; +import qs from 'query-string'; import { EuiButton, @@ -66,6 +66,9 @@ const HEADERS = { size: i18n.translate('xpack.idxMgmt.indexTable.headers.storageSizeHeader', { defaultMessage: 'Storage size', }), + data_stream: i18n.translate('xpack.idxMgmt.indexTable.headers.dataStreamHeader', { + defaultMessage: 'Data stream', + }), }; export class IndexTable extends Component { @@ -97,17 +100,14 @@ export class IndexTable extends Component { componentDidMount() { this.props.loadIndices(); - this.interval = setInterval(this.props.reloadIndices, REFRESH_RATE_INDEX_LIST); - const { - filterChanged, - filterFromURI, - showHiddenIndicesChanged, - showHiddenIndices, - location, - } = this.props; - - if (filterFromURI) { - const decodedFilter = decodeURIComponent(filterFromURI); + this.interval = setInterval( + () => this.props.reloadIndices(this.props.indices.map((i) => i.name)), + REFRESH_RATE_INDEX_LIST + ); + const { location, filterChanged } = this.props; + const { filter } = qs.parse((location && location.search) || ''); + if (filter) { + const decodedFilter = decodeURIComponent(filter); try { const filter = EuiSearchBar.Query.parse(decodedFilter); @@ -116,17 +116,30 @@ export class IndexTable extends Component { this.setState({ filterError: e }); } } - - // Check if the we have the includeHidden query param - const { includeHidden } = parse((location && location.search) || ''); - const nextValue = includeHidden === 'true'; - if (nextValue !== showHiddenIndices) { - showHiddenIndicesChanged(nextValue); - } } componentWillUnmount() { clearInterval(this.interval); } + + readURLParams() { + const { location } = this.props; + const { includeHiddenIndices } = qs.parse((location && location.search) || ''); + return { + includeHiddenIndices: includeHiddenIndices === 'true', + }; + } + + setIncludeHiddenParam(hidden) { + const { pathname, search } = this.props.location; + const params = qs.parse(search); + if (hidden) { + params.includeHiddenIndices = 'true'; + } else { + delete params.includeHiddenIndices; + } + this.props.history.push(pathname + '?' + qs.stringify(params)); + } + onSort = (column) => { const { sortField, isSortAscending, sortChanged } = this.props; @@ -416,8 +429,6 @@ export class IndexTable extends Component { render() { const { filter, - showHiddenIndices, - showHiddenIndicesChanged, indices, loadIndices, indicesLoading, @@ -426,6 +437,8 @@ export class IndexTable extends Component { pager, } = this.props; + const { includeHiddenIndices } = this.readURLParams(); + let emptyState; if (indicesLoading) { @@ -477,8 +490,8 @@ export class IndexTable extends Component { showHiddenIndicesChanged(event.target.checked)} + checked={includeHiddenIndices} + onChange={(event) => this.setIncludeHiddenParam(event.target.checked)} label={ { + filters: Filters; + onChange(filters: Filters): void; +} + +export type Filters = { + [key in T]: Filter; +}; + +export function FilterListButton({ onChange, filters }: Props) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const activeFilters = Object.values(filters).filter((v) => (v as Filter).checked === 'on'); + + const onButtonClick = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const toggleFilter = (filter: T) => { + const previousValue = filters[filter].checked; + onChange({ + ...filters, + [filter]: { + ...filters[filter], + checked: previousValue === 'on' ? 'off' : 'on', + }, + }); + }; + + const button = ( + 0} + numActiveFilters={activeFilters.length} + data-test-subj="viewButton" + > + + + ); + + return ( + +
+ {Object.entries(filters).map(([filter, item], index) => ( + toggleFilter(filter as T)} + data-test-subj="filterItem" + > + {(item as Filter).name} + + ))} +
+
+ ); +} diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts new file mode 100644 index 0000000000000..dcaba319bb21a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './filter_list_button'; +export * from './template_content_indicator'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx new file mode 100644 index 0000000000000..78e33d7940bd4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx @@ -0,0 +1,51 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; + +interface Props { + mappings: boolean; + settings: boolean; + aliases: boolean; +} + +const texts = { + settings: i18n.translate('xpack.idxMgmt.templateContentIndicator.indexSettingsTooltipLabel', { + defaultMessage: 'Index settings', + }), + mappings: i18n.translate('xpack.idxMgmt.templateContentIndicator.mappingsTooltipLabel', { + defaultMessage: 'Mappings', + }), + aliases: i18n.translate('xpack.idxMgmt.templateContentIndicator.aliasesTooltipLabel', { + defaultMessage: 'Aliases', + }), +}; + +export const TemplateContentIndicator = ({ mappings, settings, aliases }: Props) => { + const getColor = (flag: boolean) => (flag ? 'primary' : 'hollow'); + + return ( + <> + + <> + M +   + + + + <> + S +   + + + + A + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts new file mode 100644 index 0000000000000..519120b559e7b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { LegacyTemplateDetails } from './template_details'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx new file mode 100644 index 0000000000000..ec2956973d4f6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx @@ -0,0 +1,327 @@ +/* + * 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, { Fragment, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCallOut, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiTab, + EuiTabs, + EuiSpacer, + EuiPopover, + EuiButton, + EuiContextMenu, +} from '@elastic/eui'; +import { + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +} from '../../../../../../../common/constants'; +import { TemplateDeserialized } from '../../../../../../../common'; +import { + TemplateDeleteModal, + SectionLoading, + SectionError, + Error, +} from '../../../../../components'; +import { useLoadIndexTemplate } from '../../../../../services/api'; +import { decodePath } from '../../../../../services/routing'; +import { SendRequestResponse } from '../../../../../../shared_imports'; +import { useServices } from '../../../../../app_context'; +import { TabSummary, TabMappings, TabSettings, TabAliases } from '../../template_details/tabs'; + +interface Props { + template: { name: string; isLegacy?: boolean }; + onClose: () => void; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + reload: () => Promise; +} + +const SUMMARY_TAB_ID = 'summary'; +const MAPPINGS_TAB_ID = 'mappings'; +const ALIASES_TAB_ID = 'aliases'; +const SETTINGS_TAB_ID = 'settings'; + +const TABS = [ + { + id: SUMMARY_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.summaryTabTitle', { + defaultMessage: 'Summary', + }), + }, + { + id: SETTINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.settingsTabTitle', { + defaultMessage: 'Settings', + }), + }, + { + id: MAPPINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.mappingsTabTitle', { + defaultMessage: 'Mappings', + }), + }, + { + id: ALIASES_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.aliasesTabTitle', { + defaultMessage: 'Aliases', + }), + }, +]; + +const tabToComponentMap: { + [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>; +} = { + [SUMMARY_TAB_ID]: TabSummary, + [SETTINGS_TAB_ID]: TabSettings, + [MAPPINGS_TAB_ID]: TabMappings, + [ALIASES_TAB_ID]: TabAliases, +}; + +const tabToUiMetricMap: { [key: string]: string } = { + [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +}; + +export const LegacyTemplateDetails: React.FunctionComponent = ({ + template: { name: templateName, isLegacy }, + onClose, + editTemplate, + cloneTemplate, + reload, +}) => { + const { uiMetricService } = useServices(); + const decodedTemplateName = decodePath(templateName); + const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( + decodedTemplateName, + isLegacy + ); + const isManaged = templateDetails?._kbnMeta.isManaged ?? false; + const [templateToDelete, setTemplateToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error as Error} + data-test-subj="sectionError" + /> + ); + } else if (templateDetails) { + const Content = tabToComponentMap[activeTab]; + const managedTemplateCallout = isManaged ? ( + + + } + color="primary" + size="s" + > + + + + + ) : null; + + content = ( + + {managedTemplateCallout} + + + {TABS.map((tab) => ( + { + uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj="tab" + > + {tab.name} + + ))} + + + + + + + ); + } + + return ( + + {templateToDelete && templateToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplateToDelete([]); + } + onClose(); + }} + templatesToDelete={templateToDelete} + /> + ) : null} + + + + +

+ {decodedTemplateName} +

+
+
+ + {content} + + + + + + + + + {templateDetails && ( + + {/* Manage templates context menu */} + setIsPopOverOpen((prev) => !prev)} + > + +
+ } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + editTemplate(templateName, isLegacy), + disabled: isManaged, + }, + { + name: i18n.translate( + 'xpack.idxMgmt.legacyTemplateDetails.cloneButtonLabel', + { + defaultMessage: 'Clone', + } + ), + icon: 'copy', + onClick: () => cloneTemplate(templateName, isLegacy), + }, + { + name: i18n.translate( + 'xpack.idxMgmt.legacyTemplateDetails.deleteButtonLabel', + { + defaultMessage: 'Delete', + } + ), + icon: 'trash', + onClick: () => + setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), + disabled: isManaged, + }, + ], + }, + ]} + /> + + + )} + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts new file mode 100644 index 0000000000000..a8499df45ce27 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { LegacyTemplateTable } from './template_table'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx new file mode 100644 index 0000000000000..92fedd5d68f00 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -0,0 +1,304 @@ +/* + * 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, { useState, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; +import { reactRouterNavigate } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { TemplateListItem } from '../../../../../../../common'; +import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants'; +import { TemplateDeleteModal } from '../../../../../components'; +import { useServices } from '../../../../../app_context'; +import { SendRequestResponse } from '../../../../../../shared_imports'; + +interface Props { + templates: TemplateListItem[]; + reload: () => Promise; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + history: ScopedHistory; +} + +export const LegacyTemplateTable: React.FunctionComponent = ({ + templates, + reload, + editTemplate, + cloneTemplate, + history, +}) => { + const { uiMetricService } = useServices(); + const [selection, setSelection] = useState([]); + const [templatesToDelete, setTemplatesToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.nameColumnTitle', { + defaultMessage: 'Name', + }), + truncateText: true, + sortable: true, + render: (name: TemplateListItem['name'], item: TemplateListItem) => { + return ( + /* eslint-disable-next-line @elastic/eui/href-or-on-click */ + uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) + )} + data-test-subj="templateDetailsLink" + > + {name} + + ); + }, + }, + { + field: 'indexPatterns', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.indexPatternsColumnTitle', { + defaultMessage: 'Index patterns', + }), + truncateText: true, + sortable: true, + render: (indexPatterns: string[]) => {indexPatterns.join(', ')}, + }, + { + field: 'ilmPolicy', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.ilmPolicyColumnTitle', { + defaultMessage: 'ILM policy', + }), + truncateText: true, + sortable: true, + render: (ilmPolicy: { name: string }) => + ilmPolicy && ilmPolicy.name ? ( + + {ilmPolicy.name} + + ) : null, + }, + { + field: 'order', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.orderColumnTitle', { + defaultMessage: 'Order', + }), + truncateText: true, + sortable: true, + }, + { + field: 'hasMappings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.mappingsColumnTitle', { + defaultMessage: 'Mappings', + }), + truncateText: true, + sortable: true, + render: (hasMappings: boolean) => (hasMappings ? : null), + }, + { + field: 'hasSettings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.settingsColumnTitle', { + defaultMessage: 'Settings', + }), + truncateText: true, + sortable: true, + render: (hasSettings: boolean) => (hasSettings ? : null), + }, + { + field: 'hasAliases', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.aliasesColumnTitle', { + defaultMessage: 'Aliases', + }), + truncateText: true, + sortable: true, + render: (hasAliases: boolean) => (hasAliases ? : null), + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionEditText', { + defaultMessage: 'Edit', + }), + isPrimary: true, + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionEditDecription', + { + defaultMessage: 'Edit this template', + } + ), + icon: 'pencil', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + editTemplate(name, isLegacy); + }, + enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + }, + { + type: 'icon', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionCloneTitle', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionCloneDescription', + { + defaultMessage: 'Clone this template', + } + ), + icon: 'copy', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + cloneTemplate(name, isLegacy); + }, + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionDeleteText', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionDeleteDecription', + { + defaultMessage: 'Delete this template', + } + ), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + setTemplatesToDelete([{ name, isLegacy }]); + }, + isPrimary: true, + enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + }, + ], + }, + ]; + + const pagination = { + initialPageSize: 20, + pageSizeOptions: [10, 20, 50], + }; + + const sorting = { + sort: { + field: 'name', + direction: 'asc', + }, + } as const; + + const selectionConfig = { + onSelectionChange: setSelection, + selectable: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + selectableMessage: (selectable: boolean) => { + if (!selectable) { + return i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + { + defaultMessage: 'You cannot delete a managed template.', + } + ); + } + return ''; + }, + }; + + const searchConfig = { + box: { + incremental: true, + }, + toolsLeft: + selection.length > 0 ? ( + + setTemplatesToDelete( + selection.map(({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => ({ + name, + isLegacy, + })) + ) + } + color="danger" + > + + + ) : undefined, + toolsRight: [ + + + , + ], + }; + + return ( + + {templatesToDelete && templatesToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplatesToDelete([]); + } + }} + templatesToDelete={templatesToDelete} + /> + ) : null} + ({ + 'data-test-subj': 'row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + data-test-subj="legacyTemplateTable" + message={ + + } + /> + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx index ed403276af564..9f51f114176fb 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx @@ -4,313 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiTab, - EuiTabs, - EuiSpacer, - EuiPopover, - EuiButton, - EuiContextMenu, -} from '@elastic/eui'; -import { - UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -} from '../../../../../../common/constants'; -import { TemplateDeserialized, IndexTemplateFormatVersion } from '../../../../../../common'; -import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; -import { useLoadIndexTemplate } from '../../../../services/api'; -import { decodePath } from '../../../../services/routing'; -import { SendRequestResponse } from '../../../../../shared_imports'; -import { useServices } from '../../../../app_context'; -import { TabSummary, TabMappings, TabSettings, TabAliases } from './tabs'; +import React from 'react'; -interface Props { - template: { name: string; formatVersion: IndexTemplateFormatVersion }; - onClose: () => void; - editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - reload: () => Promise; -} - -const SUMMARY_TAB_ID = 'summary'; -const MAPPINGS_TAB_ID = 'mappings'; -const ALIASES_TAB_ID = 'aliases'; -const SETTINGS_TAB_ID = 'settings'; - -const TABS = [ - { - id: SUMMARY_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.summaryTabTitle', { - defaultMessage: 'Summary', - }), - }, - { - id: SETTINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.settingsTabTitle', { - defaultMessage: 'Settings', - }), - }, - { - id: MAPPINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.mappingsTabTitle', { - defaultMessage: 'Mappings', - }), - }, - { - id: ALIASES_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.aliasesTabTitle', { - defaultMessage: 'Aliases', - }), - }, -]; - -const tabToComponentMap: { - [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>; -} = { - [SUMMARY_TAB_ID]: TabSummary, - [SETTINGS_TAB_ID]: TabSettings, - [MAPPINGS_TAB_ID]: TabMappings, - [ALIASES_TAB_ID]: TabAliases, -}; - -const tabToUiMetricMap: { [key: string]: string } = { - [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -}; - -export const TemplateDetails: React.FunctionComponent = ({ - template: { name: templateName, formatVersion }, - onClose, - editTemplate, - cloneTemplate, - reload, -}) => { - const { uiMetricService } = useServices(); - const decodedTemplateName = decodePath(templateName); - const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( - decodedTemplateName, - formatVersion - ); - const isManaged = templateDetails?.isManaged; - const [templateToDelete, setTemplateToDelete] = useState< - Array<{ name: string; formatVersion: IndexTemplateFormatVersion }> - >([]); - const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); - const [isPopoverOpen, setIsPopOverOpen] = useState(false); - - let content; - - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - - } - error={error as Error} - data-test-subj="sectionError" - /> - ); - } else if (templateDetails) { - const Content = tabToComponentMap[activeTab]; - const managedTemplateCallout = isManaged ? ( - - - } - color="primary" - size="s" - > - - - - - ) : null; - - content = ( - - {managedTemplateCallout} - - - {TABS.map((tab) => ( - { - uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); - setActiveTab(tab.id); - }} - isSelected={tab.id === activeTab} - key={tab.id} - data-test-subj="tab" - > - {tab.name} - - ))} - - - - - - - ); - } - - return ( - - {templateToDelete && templateToDelete.length > 0 ? ( - { - if (data && data.hasDeletedTemplates) { - reload(); - } else { - setTemplateToDelete([]); - } - onClose(); - }} - templatesToDelete={templateToDelete} - /> - ) : null} - - - - -

- {decodedTemplateName} -

-
-
- - {content} - - - - - - - - - {templateDetails && ( - - {/* Manage templates context menu */} - setIsPopOverOpen((prev) => !prev)} - > - - - } - isOpen={isPopoverOpen} - closePopover={() => setIsPopOverOpen(false)} - panelPaddingSize="none" - withTitle - anchorPosition="rightUp" - repositionOnScroll - > - editTemplate(templateName, formatVersion), - disabled: isManaged, - }, - { - name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', { - defaultMessage: 'Clone', - }), - icon: 'copy', - onClick: () => cloneTemplate(templateName, formatVersion), - }, - { - name: i18n.translate( - 'xpack.idxMgmt.templateDetails.deleteButtonLabel', - { - defaultMessage: 'Delete', - } - ), - icon: 'trash', - onClick: () => - setTemplateToDelete([{ name: decodedTemplateName, formatVersion }]), - disabled: isManaged, - }, - ], - }, - ]} - /> - - - )} - - -
-
- ); +export const TemplateDetails: React.FunctionComponent = () => { + // TODO new (V2) templatte details + return null; }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index bc942b6b3f55b..fc3d5125e3066 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -7,18 +7,20 @@ import React, { Fragment, useState, useEffect, useMemo } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { ScopedHistory } from 'kibana/public'; import { EuiEmptyPrompt, EuiSpacer, EuiTitle, EuiText, - EuiSwitch, EuiFlexItem, EuiFlexGroup, + EuiButton, } from '@elastic/eui'; import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; -import { IndexTemplateFormatVersion } from '../../../../../common'; +import { TemplateListItem } from '../../../../../common'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; import { useServices } from '../../../app_context'; @@ -27,14 +29,20 @@ import { getTemplateListLink, getTemplateCloneLink, } from '../../../services/routing'; -import { getFormatVersionFromQueryparams } from '../../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../../lib/index_templates'; import { TemplateTable } from './template_table'; -import { TemplateDetails } from './template_details'; +import { LegacyTemplateTable } from './legacy_templates/template_table'; +import { LegacyTemplateDetails } from './legacy_templates/template_details'; +import { FilterListButton, Filters } from './components'; +type FilterName = 'composable' | 'system'; interface MatchParams { templateName?: string; } +const stripOutSystemTemplates = (templates: TemplateListItem[]): TemplateListItem[] => + templates.filter((template) => !template.name.startsWith('.')); + export const TemplateList: React.FunctionComponent> = ({ match: { params: { templateName }, @@ -43,121 +51,188 @@ export const TemplateList: React.FunctionComponent { const { uiMetricService } = useServices(); - const { error, isLoading, data: templates, sendRequest: reload } = useLoadIndexTemplates(); - const queryParamsFormatVersion = getFormatVersionFromQueryparams(location); + const { error, isLoading, data: allTemplates, sendRequest: reload } = useLoadIndexTemplates(); - let content; + const [filters, setFilters] = useState>({ + composable: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewComposableTemplateLabel', { + defaultMessage: 'Composable templates', + }), + checked: 'on', + }, + system: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewSystemTemplateLabel', { + defaultMessage: 'System templates', + }), + checked: 'off', + }, + }); - const [showSystemTemplates, setShowSystemTemplates] = useState(false); + const filteredTemplates = useMemo(() => { + if (!allTemplates) { + return { templates: [], legacyTemplates: [] }; + } - // Filter out system index templates - const filteredTemplates = useMemo( - () => (templates ? templates.filter((template) => !template.name.startsWith('.')) : []), - [templates] - ); + return filters.system.checked === 'on' + ? allTemplates + : { + templates: stripOutSystemTemplates(allTemplates.templates), + legacyTemplates: stripOutSystemTemplates(allTemplates.legacyTemplates), + }; + }, [allTemplates, filters.system.checked]); + + const showComposableTemplateTable = filters.composable.checked === 'on'; + + const selectedTemplate = Boolean(templateName) + ? { + name: templateName!, + isLegacy: getIsLegacyFromQueryParams(location), + } + : null; + + const isLegacyTemplateDetailsVisible = selectedTemplate !== null && selectedTemplate.isLegacy; + const hasTemplates = + allTemplates && (allTemplates.legacyTemplates.length > 0 || allTemplates.templates.length > 0); const closeTemplateDetails = () => { history.push(getTemplateListLink()); }; - const editTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => { - history.push(getTemplateEditLink(name, formatVersion)); + const editTemplate = (name: string, isLegacy?: boolean) => { + history.push(getTemplateEditLink(name, isLegacy)); }; - const cloneTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => { - history.push(getTemplateCloneLink(name, formatVersion)); + const cloneTemplate = (name: string, isLegacy?: boolean) => { + history.push(getTemplateCloneLink(name, isLegacy)); }; - // Track component loaded - useEffect(() => { - uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD); - }, [uiMetricService]); + const renderHeader = () => ( + + + + + + + + + + filters={filters} onChange={setFilters} /> + + + + + + + + ); - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - + showComposableTemplateTable ? ( + <> + + + + ) : null; + + const renderLegacyTemplatesTable = () => ( + <> + + +

- } - error={error as Error} +

+
+ + - ); - } else if (Array.isArray(templates) && templates.length === 0) { - content = ( - + + ); + + const renderContent = () => { + if (isLoading) { + return ( + + + + ); + } else if (error) { + return ( + - - } - data-test-subj="emptyPrompt" - /> - ); - } else if (Array.isArray(templates) && templates.length > 0) { - content = ( - - - - - - - - - - - setShowSystemTemplates(event.target.checked)} - label={ - - } - /> - - - - + ); + } else if (!hasTemplates) { + return ( + + + + } + data-test-subj="emptyPrompt" /> - - ); - } + ); + } else { + return ( + + {/* Header */} + {renderHeader()} + + {/* Composable index templates table */} + {renderTemplatesTable()} + + {/* Legacy index templates table */} + {renderLegacyTemplatesTable()} + + ); + } + }; + + // Track component loaded + useEffect(() => { + uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD); + }, [uiMetricService]); return (
- {content} - {templateName && queryParamsFormatVersion !== undefined && ( - Promise; - editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; } -export const TemplateTable: React.FunctionComponent = ({ - templates, - reload, - editTemplate, - cloneTemplate, -}) => { - const { uiMetricService } = useServices(); - const [selection, setSelection] = useState([]); +export const TemplateTable: React.FunctionComponent = ({ templates, reload }) => { const [templatesToDelete, setTemplatesToDelete] = useState< - Array<{ name: string; formatVersion: IndexTemplateFormatVersion }> + Array<{ name: string; isLegacy?: boolean }> >([]); const columns: Array> = [ @@ -42,18 +31,6 @@ export const TemplateTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - render: (name: TemplateListItem['name'], item: TemplateListItem) => { - return ( - /* eslint-disable-next-line @elastic/eui/href-or-on-click */ - uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)} - > - {name} - - ); - }, }, { field: 'indexPatterns', @@ -86,90 +63,36 @@ export const TemplateTable: React.FunctionComponent = ({ ) : null, }, { - field: 'order', - name: i18n.translate('xpack.idxMgmt.templateList.table.orderColumnTitle', { - defaultMessage: 'Order', - }), - truncateText: true, - sortable: true, - }, - { - field: 'hasMappings', - name: i18n.translate('xpack.idxMgmt.templateList.table.mappingsColumnTitle', { - defaultMessage: 'Mappings', + field: 'composedOf', + name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', { + defaultMessage: 'Components', }), truncateText: true, sortable: true, - render: (hasMappings: boolean) => (hasMappings ? : null), + render: (composedOf: string[] = []) => {composedOf.join(', ')}, }, { - field: 'hasSettings', - name: i18n.translate('xpack.idxMgmt.templateList.table.settingsColumnTitle', { - defaultMessage: 'Settings', + field: 'priority', + name: i18n.translate('xpack.idxMgmt.templateList.table.priorityColumnTitle', { + defaultMessage: 'Priority', }), truncateText: true, sortable: true, - render: (hasSettings: boolean) => (hasSettings ? : null), }, { - field: 'hasAliases', - name: i18n.translate('xpack.idxMgmt.templateList.table.aliasesColumnTitle', { - defaultMessage: 'Aliases', + field: 'hasMappings', + name: i18n.translate('xpack.idxMgmt.templateList.table.overridesColumnTitle', { + defaultMessage: 'Overrides', }), truncateText: true, - sortable: true, - render: (hasAliases: boolean) => (hasAliases ? : null), - }, - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', { - defaultMessage: 'Actions', - }), - actions: [ - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', { - defaultMessage: 'Edit', - }), - isPrimary: true, - description: i18n.translate('xpack.idxMgmt.templateList.table.actionEditDecription', { - defaultMessage: 'Edit this template', - }), - icon: 'pencil', - type: 'icon', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - editTemplate(name, formatVersion); - }, - enabled: ({ isManaged }: TemplateListItem) => !isManaged, - }, - { - type: 'icon', - name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', { - defaultMessage: 'Clone', - }), - description: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneDescription', { - defaultMessage: 'Clone this template', - }), - icon: 'copy', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - cloneTemplate(name, formatVersion); - }, - }, - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteText', { - defaultMessage: 'Delete', - }), - description: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteDecription', { - defaultMessage: 'Delete this template', - }), - icon: 'trash', - color: 'danger', - type: 'icon', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - setTemplatesToDelete([{ name, formatVersion }]); - }, - isPrimary: true, - enabled: ({ isManaged }: TemplateListItem) => !isManaged, - }, - ], + sortable: false, + render: (_, item) => ( + + ), }, ]; @@ -185,70 +108,10 @@ export const TemplateTable: React.FunctionComponent = ({ }, } as const; - const selectionConfig = { - onSelectionChange: setSelection, - selectable: ({ isManaged }: TemplateListItem) => !isManaged, - selectableMessage: (selectable: boolean) => { - if (!selectable) { - return i18n.translate('xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip', { - defaultMessage: 'You cannot delete a managed template.', - }); - } - return ''; - }, - }; - const searchConfig = { box: { incremental: true, }, - toolsLeft: - selection.length > 0 ? ( - - setTemplatesToDelete( - selection.map(({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => ({ - name, - formatVersion, - })) - ) - } - color="danger" - > - - - ) : undefined, - toolsRight: [ - - - , - - - , - ], }; return ( @@ -271,8 +134,7 @@ export const TemplateTable: React.FunctionComponent = ({ columns={columns} search={searchConfig} sorting={sorting} - isSelectable={true} - selection={selectionConfig} + isSelectable={false} pagination={pagination} rowProps={() => ({ 'data-test-subj': 'row', diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index b69e441feb176..8bdd230f89952 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -8,12 +8,12 @@ import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common'; +import { TemplateDeserialized } from '../../../../common'; import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { decodePath, getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; -import { getFormatVersionFromQueryparams } from '../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { name: string; @@ -27,14 +27,13 @@ export const TemplateClone: React.FunctionComponent { const decodedTemplateName = decodePath(name); - const formatVersion = - getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT; + const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); const { error: templateToCloneError, data: templateToClone, isLoading } = useLoadIndexTemplate( decodedTemplateName, - formatVersion + isLegacy ); const onSave = async (template: TemplateDeserialized) => { @@ -52,7 +51,7 @@ export const TemplateClone: React.FunctionComponent { diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index 27341685f3dc0..f567b9835d53d 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -33,7 +33,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h return; } - history.push(getTemplateDetailsLink(name, template._kbnMeta.formatVersion)); + history.push(getTemplateDetailsLink(name, template._kbnMeta.isLegacy)); }; const clearSaveError = () => { diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 9ad26d0af802d..d3e539989bc96 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -8,12 +8,12 @@ import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common'; +import { TemplateDeserialized } from '../../../../common'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { decodePath, getTemplateDetailsLink } from '../../services/routing'; import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; -import { getFormatVersionFromQueryparams } from '../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { name: string; @@ -27,16 +27,12 @@ export const TemplateEdit: React.FunctionComponent { const decodedTemplateName = decodePath(name); - const formatVersion = - getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT; + const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); - const { error, data: template, isLoading } = useLoadIndexTemplate( - decodedTemplateName, - formatVersion - ); + const { error, data: template, isLoading } = useLoadIndexTemplate(decodedTemplateName, isLegacy); useEffect(() => { breadcrumbService.setBreadcrumbs('templateEdit'); @@ -55,7 +51,7 @@ export const TemplateEdit: React.FunctionComponent { @@ -87,7 +83,10 @@ export const TemplateEdit: React.FunctionComponent ); } else if (template) { - const { name: templateName, isManaged } = template; + const { + name: templateName, + _kbnMeta: { isManaged }, + } = template; const isSystemTemplate = templateName && templateName.startsWith('.'); if (isManaged) { diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 181707b3661b4..3961942b83ea3 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -37,11 +37,7 @@ import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; import { useRequest, sendRequest } from './use_request'; import { httpService } from './http'; import { UiMetricService } from './ui_metric'; -import { - TemplateDeserialized, - TemplateListItem, - IndexTemplateFormatVersion, -} from '../../../common'; +import { TemplateDeserialized, TemplateListItem } from '../../../common'; import { IndexMgmtMetricsType } from '../../types'; // Temporary hack to provide the uiMetricService instance to this file. @@ -214,17 +210,15 @@ export async function loadIndexData(type: string, indexName: string) { } export function useLoadIndexTemplates() { - return useRequest({ - path: `${API_BASE_PATH}/templates`, + return useRequest<{ templates: TemplateListItem[]; legacyTemplates: TemplateListItem[] }>({ + path: `${API_BASE_PATH}/index-templates`, method: 'get', }); } -export async function deleteTemplates( - templates: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }> -) { +export async function deleteTemplates(templates: Array<{ name: string; isLegacy?: boolean }>) { const result = sendRequest({ - path: `${API_BASE_PATH}/delete-templates`, + path: `${API_BASE_PATH}/delete-index-templates`, method: 'post', body: { templates }, }); @@ -236,23 +230,20 @@ export async function deleteTemplates( return result; } -export function useLoadIndexTemplate( - name: TemplateDeserialized['name'], - formatVersion: IndexTemplateFormatVersion -) { +export function useLoadIndexTemplate(name: TemplateDeserialized['name'], isLegacy?: boolean) { return useRequest({ - path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`, + path: `${API_BASE_PATH}/index-templates/${encodeURIComponent(name)}`, method: 'get', query: { - v: formatVersion, + legacy: isLegacy, }, }); } export async function saveTemplate(template: TemplateDeserialized, isClone?: boolean) { const result = await sendRequest({ - path: `${API_BASE_PATH}/templates`, - method: 'put', + path: `${API_BASE_PATH}/index-templates`, + method: 'post', body: JSON.stringify(template), }); @@ -266,7 +257,7 @@ export async function saveTemplate(template: TemplateDeserialized, isClone?: boo export async function updateTemplate(template: TemplateDeserialized) { const { name } = template; const result = await sendRequest({ - path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`, + path: `${API_BASE_PATH}/index-templates/${encodeURIComponent(name)}`, method: 'put', body: JSON.stringify(template), }); diff --git a/x-pack/plugins/index_management/public/application/services/breadcrumbs.ts b/x-pack/plugins/index_management/public/application/services/breadcrumbs.ts index 8128ccd545dce..e3d0058b2b9b0 100644 --- a/x-pack/plugins/index_management/public/application/services/breadcrumbs.ts +++ b/x-pack/plugins/index_management/public/application/services/breadcrumbs.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { BASE_PATH } from '../../../common/constants'; import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; @@ -28,7 +27,7 @@ class BreadcrumbService { text: i18n.translate('xpack.idxMgmt.breadcrumb.homeLabel', { defaultMessage: 'Index Management', }), - href: `#${BASE_PATH}`, + href: `/`, }, ]; @@ -38,7 +37,7 @@ class BreadcrumbService { text: i18n.translate('xpack.idxMgmt.breadcrumb.templatesLabel', { defaultMessage: 'Templates', }), - href: `#${BASE_PATH}templates`, + href: `/templates`, }, ]; diff --git a/x-pack/plugins/index_management/public/application/services/navigation.ts b/x-pack/plugins/index_management/public/application/services/navigation.ts index 8bd9fc2432232..3b4977bb63751 100644 --- a/x-pack/plugins/index_management/public/application/services/navigation.ts +++ b/x-pack/plugins/index_management/public/application/services/navigation.ts @@ -3,22 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { BASE_PATH } from '../../../common/constants'; export const getIndexListUri = (filter: any) => { if (filter) { // React router tries to decode url params but it can't because the browser partially // decodes them. So we have to encode both the URL and the filter to get it all to // work correctly for filters with URL unsafe characters in them. - return encodeURI(`#${BASE_PATH}indices/filter/${encodeURIComponent(filter)}`); + return encodeURI(`/indices/filter/${encodeURIComponent(filter)}`); } // If no filter, URI is already safe so no need to encode. - return `#${BASE_PATH}indices`; + return '/indices'; }; export const getILMPolicyPath = (policyName: string) => { - return encodeURI( - `#/management/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}` - ); + return encodeURI(`/policies/edit/${encodeURIComponent(policyName)}`); }; diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index a6d8f67751cd1..a999c58f5bb42 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -3,36 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { BASE_PATH } from '../../../common/constants'; -import { IndexTemplateFormatVersion } from '../../../common'; -export const getTemplateListLink = () => { - return `${BASE_PATH}templates`; -}; +export const getTemplateListLink = () => `/templates`; // Need to add some additonal encoding/decoding logic to work with React Router // For background, see: https://github.com/ReactTraining/history/issues/505 -export const getTemplateDetailsLink = ( - name: string, - formatVersion: IndexTemplateFormatVersion, - withHash = false -) => { - const baseUrl = `${BASE_PATH}templates/${encodeURIComponent( - encodeURIComponent(name) - )}?v=${formatVersion}`; - const url = withHash ? `#${baseUrl}` : baseUrl; +export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHash = false) => { + const baseUrl = `/templates/${encodeURIComponent(encodeURIComponent(name))}`; + let url = withHash ? `#${baseUrl}` : baseUrl; + if (isLegacy) { + url = `${url}?legacy=${isLegacy}`; + } return encodeURI(url); }; -export const getTemplateEditLink = (name: string, formatVersion: IndexTemplateFormatVersion) => { +export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { return encodeURI( - `${BASE_PATH}edit_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}` + `/edit_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` ); }; -export const getTemplateCloneLink = (name: string, formatVersion: IndexTemplateFormatVersion) => { +export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { return encodeURI( - `${BASE_PATH}clone_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}` + `/clone_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` ); }; diff --git a/x-pack/plugins/index_management/public/application/services/sort_table.ts b/x-pack/plugins/index_management/public/application/services/sort_table.ts index ed8c5bb146f53..429f2961c4521 100644 --- a/x-pack/plugins/index_management/public/application/services/sort_table.ts +++ b/x-pack/plugins/index_management/public/application/services/sort_table.ts @@ -14,7 +14,8 @@ type SortField = | 'replica' | 'documents' | 'size' - | 'primary_size'; + | 'primary_size' + | 'data_stream'; type Unit = 'kb' | 'mb' | 'gb' | 'tb' | 'pb'; @@ -55,6 +56,7 @@ const sorters = { documents: numericSort('documents'), size: byteSort('size'), primary_size: byteSort('primary_size'), + data_stream: stringSort('data_stream'), }; export const sortTable = (array = [], sortField: SortField, isSortAscending: boolean) => { diff --git a/x-pack/plugins/index_management/public/application/store/actions/reload_indices.js b/x-pack/plugins/index_management/public/application/store/actions/reload_indices.js index 11329ece8f59f..d70e811ff24e5 100644 --- a/x-pack/plugins/index_management/public/application/store/actions/reload_indices.js +++ b/x-pack/plugins/index_management/public/application/store/actions/reload_indices.js @@ -6,15 +6,13 @@ import { createAction } from 'redux-actions'; import { i18n } from '@kbn/i18n'; -import { getIndexNamesForCurrentPage } from '../selectors'; import { reloadIndices as request } from '../../services'; import { loadIndices } from './load_indices'; import { notificationService } from '../../services/notification'; export const reloadIndicesSuccess = createAction('INDEX_MANAGEMENT_RELOAD_INDICES_SUCCESS'); -export const reloadIndices = (indexNames) => async (dispatch, getState) => { +export const reloadIndices = (indexNames) => async (dispatch) => { let indices; - indexNames = indexNames || getIndexNamesForCurrentPage(getState()); try { indices = await request(indexNames); } catch (error) { diff --git a/x-pack/plugins/index_management/public/application/store/actions/table_state.js b/x-pack/plugins/index_management/public/application/store/actions/table_state.js index 70e0de74d0278..2127d8cd36b1e 100644 --- a/x-pack/plugins/index_management/public/application/store/actions/table_state.js +++ b/x-pack/plugins/index_management/public/application/store/actions/table_state.js @@ -17,8 +17,4 @@ export const pageSizeChanged = createAction('INDEX_MANAGEMENT_PAGE_SIZE_CHANGED' export const sortChanged = createAction('INDEX_MANAGEMENT_SORT_CHANGED'); -export const showHiddenIndicesChanged = createAction( - 'INDEX_MANAGEMENT_SHOW_HIDDEN_INDICES_CHANGED' -); - export const toggleChanged = createAction('INDEX_MANAGEMENT_TOGGLE_CHANGED'); diff --git a/x-pack/plugins/index_management/public/application/store/middlewares/index.ts b/x-pack/plugins/index_management/public/application/store/middlewares/index.ts deleted file mode 100644 index 06d77a9c3d348..0000000000000 --- a/x-pack/plugins/index_management/public/application/store/middlewares/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { syncUrlHashQueryParam } from './sync_url_hash_query_param.js'; diff --git a/x-pack/plugins/index_management/public/application/store/middlewares/sync_url_hash_query_param.js.ts b/x-pack/plugins/index_management/public/application/store/middlewares/sync_url_hash_query_param.js.ts deleted file mode 100644 index 145b4b6c9a8bc..0000000000000 --- a/x-pack/plugins/index_management/public/application/store/middlewares/sync_url_hash_query_param.js.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import * as q from 'query-string'; -import { Middleware } from 'redux'; -// @ts-ignore -import { showHiddenIndicesChanged } from '../actions'; - -export const syncUrlHashQueryParam: Middleware = () => (next) => (action) => { - if (action.type === String(showHiddenIndicesChanged)) { - const { url, query } = q.parseUrl(window.location.hash); - if (action.payload.showHiddenIndices) { - query.includeHidden = 'true'; - } else { - delete query.includeHidden; - } - window.location.hash = url + '?' + q.stringify(query); - } - next(action); -}; diff --git a/x-pack/plugins/index_management/public/application/store/reducers/table_state.js b/x-pack/plugins/index_management/public/application/store/reducers/table_state.js index e90fa72aa62fe..bfb37aa56b129 100644 --- a/x-pack/plugins/index_management/public/application/store/reducers/table_state.js +++ b/x-pack/plugins/index_management/public/application/store/reducers/table_state.js @@ -10,7 +10,6 @@ import { pageChanged, pageSizeChanged, sortChanged, - showHiddenIndicesChanged, toggleChanged, } from '../actions'; @@ -20,7 +19,6 @@ export const defaultTableState = { currentPage: 0, sortField: 'index.name', isSortAscending: true, - showHiddenIndices: false, }; export const tableState = handleActions( @@ -33,14 +31,6 @@ export const tableState = handleActions( currentPage: 0, }; }, - [showHiddenIndicesChanged](state, action) { - const { showHiddenIndices } = action.payload; - - return { - ...state, - showHiddenIndices, - }; - }, [toggleChanged](state, action) { const { toggleName, toggleValue } = action.payload; const toggleNameToVisibleMap = { ...state.toggleNameToVisibleMap }; diff --git a/x-pack/plugins/index_management/public/application/store/selectors/index.d.ts b/x-pack/plugins/index_management/public/application/store/selectors/index.d.ts index 999b11c5d6665..37aaea0e7e3d8 100644 --- a/x-pack/plugins/index_management/public/application/store/selectors/index.d.ts +++ b/x-pack/plugins/index_management/public/application/store/selectors/index.d.ts @@ -6,3 +6,5 @@ import { ExtensionsService } from '../../../services'; export declare function setExtensionsService(extensionsService: ExtensionsService): any; + +export const getFilteredIndices: (state: any, props: any) => any; diff --git a/x-pack/plugins/index_management/public/application/store/selectors/index.js b/x-pack/plugins/index_management/public/application/store/selectors/index.js index c1011680d4da2..c80658581dbee 100644 --- a/x-pack/plugins/index_management/public/application/store/selectors/index.js +++ b/x-pack/plugins/index_management/public/application/store/selectors/index.js @@ -6,6 +6,7 @@ import { Pager, EuiSearchBar } from '@elastic/eui'; import { createSelector } from 'reselect'; +import * as qs from 'query-string'; import { indexStatusLabels } from '../../lib/index_status_labels'; import { sortTable } from '../../services'; @@ -35,6 +36,7 @@ export const getIndexByIndexName = (state, name) => getIndices(state)[name]; export const getFilteredIds = (state) => state.indices.filteredIds; export const getRowStatuses = (state) => state.rowStatus; export const getTableState = (state) => state.tableState; +export const getTableLocationProp = (_, props) => props.location; export const getAllIds = (state) => state.indices.allIds; export const getIndexStatusByIndexName = (state, indexName) => { const indices = getIndices(state); @@ -79,18 +81,24 @@ const filterByToggles = (indices, toggleNameToVisibleMap) => { }); }); }; -const getFilteredIndices = createSelector( + +export const getFilteredIndices = createSelector( getIndices, getAllIds, getTableState, - (indices, allIds, tableState) => { + getTableLocationProp, + (indices, allIds, tableState, tableLocation) => { let indexArray = allIds.map((indexName) => indices[indexName]); indexArray = filterByToggles(indexArray, tableState.toggleNameToVisibleMap); - const systemFilteredIndexes = tableState.showHiddenIndices + const { includeHiddenIndices: includeHiddenParam } = qs.parse(tableLocation.search); + const includeHidden = includeHiddenParam === 'true'; + const filteredIndices = includeHidden ? indexArray - : indexArray.filter((index) => !(index.name + '').startsWith('.') && !index.hidden); + : indexArray.filter((index) => { + return !(index.name + '').startsWith('.') && !index.hidden; + }); const filter = tableState.filter || EuiSearchBar.Query.MATCH_ALL; - return EuiSearchBar.Query.execute(filter, systemFilteredIndexes, { + return EuiSearchBar.Query.execute(filter, filteredIndices, { defaultFields: defaultFilterFields, }); } @@ -133,29 +141,8 @@ export const getPageOfIndices = createSelector( } ); -export const getIndexNamesForCurrentPage = createSelector(getPageOfIndices, (pageOfIndices) => { - return pageOfIndices.map((index) => index.name); -}); - -export const getHasNextPage = createSelector(getPager, (pager) => { - return pager.hasNextPage; -}); - -export const getHasPreviousPage = createSelector(getPager, (pager) => { - return pager.hasPreviousPage; -}); - -export const getCurrentPage = createSelector(getPager, (pager) => { - return pager.currentPage; -}); - export const getFilter = createSelector(getTableState, ({ filter }) => filter); -export const showHiddenIndices = createSelector( - getTableState, - ({ showHiddenIndices }) => showHiddenIndices -); - export const isSortAscending = createSelector( getTableState, ({ isSortAscending }) => isSortAscending diff --git a/x-pack/plugins/index_management/public/application/store/selectors/indices_filter.test.ts b/x-pack/plugins/index_management/public/application/store/selectors/indices_filter.test.ts new file mode 100644 index 0000000000000..f230ddd18e9eb --- /dev/null +++ b/x-pack/plugins/index_management/public/application/store/selectors/indices_filter.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { ExtensionsService } from '../../../services'; +import { getFilteredIndices, setExtensionsService } from '.'; +// @ts-ignore +import { defaultTableState } from '../reducers/table_state'; + +describe('getFilteredIndices selector', () => { + let extensionService: ExtensionsService; + beforeAll(() => { + extensionService = new ExtensionsService(); + extensionService.setup(); + setExtensionsService(extensionService); + }); + + const state = { + tableState: { ...defaultTableState }, + indices: { + byId: { + test: { name: 'index1', hidden: true }, + anotherTest: { name: 'index2', hidden: false }, + aTest: { name: 'index3' }, + aFinalTest: { name: '.index4' }, + }, + allIds: ['test', 'anotherTest', 'aTest', 'aFinalTest'], + }, + }; + + it('filters out hidden indices', () => { + expect(getFilteredIndices(state, { location: { search: '' } })).toEqual([ + { name: 'index2', hidden: false }, + { name: 'index3' }, + ]); + }); + + it('includes hidden indices', () => { + expect( + getFilteredIndices(state, { location: { search: '?includeHiddenIndices=true' } }) + ).toEqual([ + { name: 'index1', hidden: true }, + { name: 'index2', hidden: false }, + { name: 'index3' }, + { name: '.index4' }, + ]); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/store/store.js b/x-pack/plugins/index_management/public/application/store/store.js index d2f24d50941c6..b189a7cf38938 100644 --- a/x-pack/plugins/index_management/public/application/store/store.js +++ b/x-pack/plugins/index_management/public/application/store/store.js @@ -9,7 +9,6 @@ import thunk from 'redux-thunk'; import { defaultTableState } from './reducers/table_state'; import { getReducer } from './reducers/'; -import { syncUrlHashQueryParam } from './middlewares'; export function indexManagementStore(services) { const toggleNameToVisibleMap = {}; @@ -17,7 +16,7 @@ export function indexManagementStore(services) { toggleNameToVisibleMap[toggleExtension.name] = false; }); const initialState = { tableState: { ...defaultTableState, toggleNameToVisibleMap } }; - const enhancers = [applyMiddleware(thunk, syncUrlHashQueryParam)]; + const enhancers = [applyMiddleware(thunk)]; window.__REDUX_DEVTOOLS_EXTENSION__ && enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__()); return createStore(getReducer(services), initialState, compose(...enhancers)); diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index b52a63a414967..ae10629e069e8 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -23,6 +23,7 @@ interface Hit { interface IndexInfo { aliases: { [aliasName: string]: unknown }; mappings: unknown; + data_stream?: string; settings: { index: { hidden: 'true' | 'false'; @@ -87,6 +88,7 @@ async function fetchIndicesCall( isFrozen: hit.sth === 'true', // sth value coming back as a string from ES aliases: aliases.length ? aliases : 'none', hidden: index.settings.index.hidden === 'true', + data_stream: index.data_stream, }; }); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index 1409fa8af2ceb..26e74847e3e05 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { TemplateDeserialized } from '../../../../common'; -import { serializeV1Template } from '../../../../common/lib'; +import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; @@ -15,35 +15,26 @@ import { templateSchema } from './validate_schemas'; const bodySchema = templateSchema; export function registerCreateRoute({ router, license, lib }: RouteDependencies) { - router.put( - { path: addBasePath('/templates'), validate: { body: bodySchema } }, + router.post( + { path: addBasePath('/index-templates'), validate: { body: bodySchema } }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; const template = req.body as TemplateDeserialized; const { - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template; - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be created.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index templates can be created.' }); } - // For now we format to V1 index templates. - // When the V2 API is ready we will only create V2 template format. - const serializedTemplate = serializeV1Template(template); - - const { - name, - order, - index_patterns, - version, - settings, - mappings, - aliases, - } = serializedTemplate; + const serializedTemplate = serializeLegacyTemplate(template); + const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; // Check that template with the same name doesn't already exist - const templateExists = await callAsCurrentUser('indices.existsTemplate', { name }); + const templateExists = await callAsCurrentUser('indices.existsTemplate', { + name: template.name, + }); if (templateExists) { return res.conflict({ @@ -51,7 +42,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', { defaultMessage: "There is already a template with name '{name}'.", values: { - name, + name: template.name, }, }) ), @@ -61,7 +52,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) try { // Otherwise create new index template const response = await callAsCurrentUser('indices.putTemplate', { - name, + name: template.name, order, body: { index_patterns, diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts index 3dc31482b4947..b5cc00ad6d8cc 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts @@ -16,7 +16,7 @@ const bodySchema = schema.object({ templates: schema.arrayOf( schema.object({ name: schema.string(), - formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]), + isLegacy: schema.maybe(schema.boolean()), }) ), }); @@ -24,7 +24,7 @@ const bodySchema = schema.object({ export function registerDeleteRoute({ router, license }: RouteDependencies) { router.post( { - path: addBasePath('/delete-templates'), + path: addBasePath('/delete-index-templates'), validate: { body: bodySchema }, }, license.guardApiRoute(async (ctx, req, res) => { @@ -35,10 +35,10 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { }; await Promise.all( - templates.map(async ({ name, formatVersion }) => { + templates.map(async ({ name, isLegacy }) => { try { - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be deleted.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index template can be deleted.' }); } await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.deleteTemplate', { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index b18a8d88d3a4a..12ec005258a62 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -5,20 +5,38 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { deserializeV1Template, deserializeTemplateList } from '../../../../common/lib'; +import { + deserializeLegacyTemplate, + deserializeLegacyTemplateList, + deserializeTemplateList, +} from '../../../../common/lib'; import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; export function registerGetAllRoute({ router, license }: RouteDependencies) { router.get( - { path: addBasePath('/templates'), validate: false }, + { path: addBasePath('/index-templates'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); - const indexTemplatesByName = await callAsCurrentUser('indices.getTemplate'); - const body = deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix); + const _legacyTemplates = await callAsCurrentUser('indices.getTemplate'); + const { index_templates: _templates } = await callAsCurrentUser('transport.request', { + path: '_index_template', + method: 'GET', + }); + + const legacyTemplates = deserializeLegacyTemplateList( + _legacyTemplates, + managedTemplatePrefix + ); + const templates = deserializeTemplateList(_templates, managedTemplatePrefix); + + const body = { + templates, + legacyTemplates, + }; return res.ok({ body }); }) @@ -31,22 +49,22 @@ const paramsSchema = schema.object({ // Require the template format version (V1 or V2) to be provided as Query param const querySchema = schema.object({ - v: schema.oneOf([schema.literal('1'), schema.literal('2')]), + legacy: schema.maybe(schema.boolean()), }); export function registerGetOneRoute({ router, license, lib }: RouteDependencies) { router.get( { - path: addBasePath('/templates/{name}'), + path: addBasePath('/index-templates/{name}'), validate: { params: paramsSchema, query: querySchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { name } = req.params as typeof paramsSchema.type; + const { name } = req.params as TypeOf; const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - const { v: version } = req.query as TypeOf; + const { legacy } = req.query as TypeOf; - if (version !== '1') { + if (!legacy) { return res.badRequest({ body: 'Only index template version 1 can be fetched.' }); } @@ -56,7 +74,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) if (indexTemplateByName[name]) { return res.ok({ - body: deserializeV1Template( + body: deserializeLegacyTemplate( { ...indexTemplateByName[name], name }, managedTemplatePrefix ), diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index 81d7aa1b4978c..5b2a0d8722e46 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { TemplateDeserialized } from '../../../../common'; -import { serializeV1Template } from '../../../../common/lib'; +import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; @@ -19,7 +19,7 @@ const paramsSchema = schema.object({ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) { router.put( { - path: addBasePath('/templates/{name}'), + path: addBasePath('/index-templates/{name}'), validate: { body: bodySchema, params: paramsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { @@ -27,14 +27,14 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) const { name } = req.params as typeof paramsSchema.type; const template = req.body as TemplateDeserialized; const { - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template; - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be edited.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index template can be edited.' }); } - const serializedTemplate = serializeV1Template(template); + const serializedTemplate = serializeLegacyTemplate(template); const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index 491a686f81177..6ab28e9021123 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -24,8 +24,8 @@ export const templateSchema = schema.object({ rollover_alias: schema.maybe(schema.string()), }) ), - isManaged: schema.maybe(schema.boolean()), _kbnMeta: schema.object({ - formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]), + isManaged: schema.maybe(schema.boolean()), + isLegacy: schema.maybe(schema.boolean()), }), }); diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index b7b1c57b6f04e..b3fb546281f1e 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -32,6 +32,7 @@ export interface Index { size: any; isFrozen: boolean; aliases: string | string[]; + data_stream?: string; [key: string]: any; } diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 055c32d5cd5e4..e2e93bfb365d4 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -5,7 +5,7 @@ */ import { getRandomString, getRandomNumber } from '../../../../test_utils'; -import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common'; +import { TemplateDeserialized } from '../../common'; export const getTemplate = ({ name = getRandomString(), @@ -14,10 +14,11 @@ export const getTemplate = ({ indexPatterns = [], template: { settings, aliases, mappings } = {}, isManaged = false, - templateFormatVersion = DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isLegacy = false, }: Partial< TemplateDeserialized & { - templateFormatVersion?: 1 | 2; + isLegacy?: boolean; + isManaged: boolean; } > = {}): TemplateDeserialized => ({ name, @@ -29,8 +30,8 @@ export const getTemplate = ({ mappings, settings, }, - isManaged, _kbnMeta: { - formatVersion: templateFormatVersion, + isManaged, + isLegacy, }, }); diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts index 6b666b39b0094..1c7dfed82783a 100644 --- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts @@ -6,6 +6,7 @@ import * as rt from 'io-ts'; import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types'; +import { metricsExplorerSeriesRT } from './metrics_explorer'; export const SnapshotNodePathRT = rt.intersection([ rt.type({ @@ -21,6 +22,7 @@ const SnapshotNodeMetricOptionalRT = rt.partial({ value: rt.union([rt.number, rt.null]), avg: rt.union([rt.number, rt.null]), max: rt.union([rt.number, rt.null]), + timeseries: metricsExplorerSeriesRT, }); const SnapshotNodeMetricRequiredRT = rt.type({ @@ -41,11 +43,18 @@ export const SnapshotNodeResponseRT = rt.type({ interval: rt.string, }); -export const InfraTimerangeInputRT = rt.type({ - interval: rt.string, - to: rt.number, - from: rt.number, -}); +export const InfraTimerangeInputRT = rt.intersection([ + rt.type({ + interval: rt.string, + to: rt.number, + from: rt.number, + }), + rt.partial({ + lookbackSize: rt.number, + ignoreLookback: rt.boolean, + forceInterval: rt.boolean, + }), +]); export const SnapshotGroupByRT = rt.array( rt.partial({ @@ -97,6 +106,7 @@ export const SnapshotRequestRT = rt.intersection([ accountId: rt.string, region: rt.string, filterQuery: rt.union([rt.string, rt.null]), + includeTimeseries: rt.boolean, }), ]); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx index fcb29e3eb1c02..9ddf422871d18 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx @@ -8,6 +8,7 @@ import React, { useMemo } from 'react'; import { EuiFlexItem } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { WaffleSortControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ToolbarProps } from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { WaffleMetricControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/metric_control'; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index bafb38459b17b..d26575f65dfec 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -35,12 +35,13 @@ export const MetricsAlertDropdown = () => { icon="tableOfContents" key="manageLink" href={kibana.services?.application?.getUrlForApp( - 'kibana#/management/insightsAndAlerting/triggersActions/alerts' + 'management/insightsAndAlerting/triggersActions/alerts' )} > , ]; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [kibana.services]); return ( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index f4c7332a88e1d..7a71bb68bc54f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -90,6 +90,7 @@ export const Expressions: React.FC = (props) => { aggregation: 'avg', }; } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [alertsContext.metadata]); const updateParams = useCallback( @@ -109,6 +110,7 @@ export const Expressions: React.FC = (props) => { timeUnit: timeUnit ?? defaultExpression.timeUnit, }); setAlertParams('criteria', exp); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( @@ -119,6 +121,7 @@ export const Expressions: React.FC = (props) => { setAlertParams('criteria', exp); } }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [setAlertParams, alertParams.criteria] ); @@ -133,6 +136,7 @@ export const Expressions: React.FC = (props) => { [setAlertParams, derivedIndexPattern] ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ onFilterChange, ]); @@ -162,6 +166,7 @@ export const Expressions: React.FC = (props) => { setTimeSize(ts || undefined); setAlertParams('criteria', criteria); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertParams.criteria, setAlertParams] ); @@ -175,6 +180,7 @@ export const Expressions: React.FC = (props) => { setTimeUnit(tu as TimeUnit); setAlertParams('criteria', criteria); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertParams.criteria, setAlertParams] ); @@ -288,6 +294,7 @@ export const Expressions: React.FC = (props) => { />
+
= ({ : (value: number) => `${value}`; }, [data]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const yAxisFormater = useCallback(createFormatterForMetric(metric), [expression]); if (loading || !data) { diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx index a3cebcf33f386..47a0f037816bc 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx @@ -35,12 +35,13 @@ export const InventoryAlertDropdown = () => { icon="tableOfContents" key="manageLink" href={kibana.services?.application?.getUrlForApp( - 'kibana#/management/insightsAndAlerting/triggersActions/alerts' + 'management/insightsAndAlerting/triggersActions/alerts' )} > , ]; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [kibana.services]); return ( diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx index f4fab113cdd17..074464fb55414 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -117,6 +117,7 @@ export const Expressions: React.FC = (props) => { timeUnit: timeUnit ?? defaultExpression.timeUnit, }); setAlertParams('criteria', exp); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( @@ -141,6 +142,7 @@ export const Expressions: React.FC = (props) => { [derivedIndexPattern, setAlertParams] ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ onFilterChange, ]); diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx index d808b4f3b64aa..b8eb73b99f45e 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx @@ -15,8 +15,8 @@ export const AlertDropdown = () => { const [flyoutVisible, setFlyoutVisible] = useState(false); const manageAlertsLinkProps = useLinkProps( { - app: 'kibana', - hash: 'management/insightsAndAlerting/triggersActions/alerts', + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', }, { hrefOnly: true, diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx index d81d11e01d4a5..609f99805fe9c 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx @@ -137,6 +137,7 @@ export const Editor: React.FC = (props) => { } else { return []; } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sourceStatus]); const updateCount = useCallback( @@ -176,6 +177,7 @@ export const Editor: React.FC = (props) => { ? [...alertParams.criteria, DEFAULT_CRITERIA] : [DEFAULT_CRITERIA]; setAlertParams('criteria', nextCriteria); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [alertParams, setAlertParams]); const removeCriterion = useCallback( @@ -185,6 +187,7 @@ export const Editor: React.FC = (props) => { }); setAlertParams('criteria', nextCriteria); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertParams, setAlertParams] ); diff --git a/x-pack/plugins/infra/public/components/header/header.tsx b/x-pack/plugins/infra/public/components/header/header.tsx index fa71426f83645..47ee1857da591 100644 --- a/x-pack/plugins/infra/public/components/header/header.tsx +++ b/x-pack/plugins/infra/public/components/header/header.tsx @@ -31,10 +31,12 @@ export const Header = ({ breadcrumbs = [], readOnlyBadge = false }: HeaderProps) const setBreadcrumbs = useCallback(() => { return chrome?.setBreadcrumbs(breadcrumbs || []); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [breadcrumbs, chrome]); const setBadge = useCallback(() => { return chrome?.setBadge(badge); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [badge, chrome]); useEffect(() => { diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts index 5fe9a45a7ceed..d5b2a0aaa61c0 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts @@ -269,6 +269,7 @@ const useFetchEntriesEffect = ( } }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const fetchNewerEntries = useCallback( throttle(() => runFetchMoreEntriesRequest(ShouldFetchMoreEntries.After), 500), [props, state.bottomCursor] @@ -330,10 +331,12 @@ const useFetchEntriesEffect = ( props.timestampsLastUpdate, ]; + /* eslint-disable react-hooks/exhaustive-deps */ useEffect(fetchNewEntriesEffect, fetchNewEntriesEffectDependencies); useEffect(fetchMoreEntriesEffect, fetchMoreEntriesEffectDependencies); useEffect(streamEntriesEffect, streamEntriesEffectDependencies); useEffect(expandRangeEffect, expandRangeEffectDependencies); + /* eslint-enable react-hooks/exhaustive-deps */ return { fetchNewerEntries, checkForNewEntries: runFetchNewEntriesRequest }; }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts index 7c903f59002dc..d5a43c0d6cffa 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts @@ -82,6 +82,7 @@ export const useLogFilterState: (props: { } return true; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [filterQueryDraft]); const serializedFilterQuery = useMemo(() => (filterQuery ? filterQuery.serializedQuery : null), [ diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 670988d680147..80aab6237518f 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -78,6 +78,7 @@ export const useLogSource = ({ [sourceId, fetch] ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const logIndicesExist = useMemo(() => (sourceStatus?.logIndexNames?.length ?? 0) > 0, [ sourceStatus, ]); @@ -87,6 +88,7 @@ export const useLogSource = ({ fields: sourceStatus?.logIndexFields ?? [], title: sourceConfiguration?.configuration.name ?? 'unknown', }), + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [sourceConfiguration, sourceStatus] ); diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts index 94e2537a67a2a..54d565d9ee223 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts @@ -76,6 +76,7 @@ export const useSourceViaHttp = ({ title: pickIndexPattern(response?.source, indexType), }; }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [response, type] ); diff --git a/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx index 2a70edc9b9a57..cfa9a711f7743 100644 --- a/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx @@ -35,6 +35,7 @@ export const useBulkGetSavedObject = (type: string) => { }; fetchData(); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx index 8313d496a0651..0efb862ad2eb4 100644 --- a/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx @@ -40,6 +40,7 @@ export const useCreateSavedObject = (type: string) => { }; save(); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx index 3f2d15b3b86aa..e353a79b19073 100644 --- a/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx @@ -29,6 +29,7 @@ export const useDeleteSavedObject = (type: string) => { }; dobj(); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx index 8b0ab45f6e6d1..8eb6db6103ed8 100644 --- a/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx @@ -37,6 +37,7 @@ export const useFindSavedObject = { title: loadDataErrorTitle, }); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [services.notifications] ); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 156c9a919440e..3c8db3f8246c0 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -127,6 +127,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { [setAutoRefresh] ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [ logEntryRate, ]); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index 11ea137c95a1f..a1d3d56beee2c 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -29,6 +29,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ const logEntryRateSeries = useMemo( () => results?.histogramBuckets ? getLogEntryRateSeriesForPartition(results, partitionId) : [], + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [results, partitionId] ); const anomalyAnnotations = useMemo( @@ -41,6 +42,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ major: [], critical: [], }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [results, partitionId] ); const totalNumberOfLogEntries = useMemo( @@ -48,6 +50,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ results?.histogramBuckets ? getTotalNumberOfLogEntriesForPartition(results, partitionId) : undefined, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [results, partitionId] ); return ( diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx index 34c4202ab8b65..f41158e114c7d 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -42,6 +42,7 @@ export const LogsSettingsPage = () => { const availableFields = useMemo( () => sourceStatus?.logIndexFields.map((field) => field.name) ?? [], + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [sourceStatus] ); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx index 6fd40a52f615f..3ef32c920e293 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -30,6 +30,7 @@ const MODAL_MARGIN = 25; export const PageViewLogInContext: React.FC = () => { const { sourceConfiguration } = useLogSourceContext(); const { textScale, textWrap } = useContext(LogViewConfiguration.Context); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const columnConfigurations = useMemo(() => sourceConfiguration?.configuration.logColumns ?? [], [ sourceConfiguration, ]); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 8b5b191ccfdd9..1452772e49ca1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -76,6 +76,7 @@ export const Layout = () => { const intervalAsString = convertIntervalToString(interval); const dataBounds = calculateBoundsFromNodes(nodes); const bounds = autoBounds ? dataBounds : boundsOverride; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 7929cf8292cb6..538cd5f7d9525 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -141,14 +141,16 @@ export const LegendControls = ({ const handleStepsChange = useCallback( (e) => { - setLegendOptions((previous) => ({ ...previous, steps: parseInt(e.target.value, 10) })); + const steps = parseInt(e.target.value, 10); + setLegendOptions((previous) => ({ ...previous, steps })); }, [setLegendOptions] ); const handlePaletteChange = useCallback( (e) => { - setLegendOptions((previous) => ({ ...previous, palette: e.target.value })); + const palette = e.target.value; + setLegendOptions((previous) => ({ ...previous, palette })); }, [setLegendOptions] ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts index 3ec63d7b2de28..721a2d5792dca 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts @@ -14,6 +14,8 @@ import { SnapshotNodeResponseRT, SnapshotNodeResponse, SnapshotGroupBy, + SnapshotRequest, + InfraTimerangeInput, } from '../../../../../common/http_api/snapshot_api'; import { InventoryItemType, @@ -37,10 +39,11 @@ export function useSnapshot( ); }; - const timerange = { + const timerange: InfraTimerangeInput = { interval: '1m', to: currentTime, from: currentTime - 360 * 1000, + lookbackSize: 20, }; const { error, loading, response, makeRequest } = useHTTPRequest( @@ -55,7 +58,8 @@ export function useSnapshot( sourceId, accountId, region, - }), + includeTimeseries: true, + } as SnapshotRequest), decodeResponse ); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx index 6a4d6521855a9..dee2e0c9f457f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx @@ -45,6 +45,7 @@ export const ChartSectionVis = ({ }: VisSectionProps) => { const isDarkMode = useUiSetting('theme:darkMode'); const [dateFormat] = useKibanaUiSetting('dateFormat'); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [ formatter, formatterTemplate, diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx index 4c75003616117..88e7c0c08e441 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx @@ -23,6 +23,7 @@ export const SubSection: FunctionComponent = ({ isLiveStreaming, stopLiveStreaming, }) => { + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const metric = useMemo(() => metrics?.find((m) => m.id === id), [id, metrics]); if (!children || !metric) { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx index ef6486eac0fdb..afee0c0498187 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx @@ -6,6 +6,7 @@ import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { euiStyled } from '../../../../../../observability/public'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; @@ -22,7 +23,7 @@ interface MetricsTimeControlsProps { } export const MetricsTimeControls = (props: MetricsTimeControlsProps) => { - const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); + const [timepickerQuickRanges] = useKibanaUiSetting(UI_SETTINGS.TIMEPICKER_QUICK_RANGES); const { onChangeTimeRange, onRefresh, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index d079b7bb93d73..2a218c1c78aa3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -87,6 +87,7 @@ export const MetricsExplorerChart = ({ [dateFormat] ), }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const yAxisFormater = useCallback(createFormatterForMetric(first(metrics)), [options]); const dataDomain = calculateDomain(series, metrics, chartOptions.stack); const domain = diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx index 7ad1d943a9896..1471efbd21e18 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IIndexPattern, UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { MetricsExplorerMetric, MetricsExplorerAggregation, @@ -61,7 +61,7 @@ export const MetricsExplorerToolbar = ({ onViewStateChange, }: Props) => { const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0; - const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); + const [timepickerQuickRanges] = useKibanaUiSetting(UI_SETTINGS.TIMEPICKER_QUICK_RANGES); const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); return ( diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index ead5644d19fa2..deae78e22c6a1 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -64,7 +64,7 @@ export class Plugin defaultMessage: 'Logs', }), euiIconType: 'logsApp', - order: 8000, + order: 8100, appRoute: '/app/logs', category: DEFAULT_APP_CATEGORIES.observability, mount: async (params: AppMountParameters) => { @@ -89,7 +89,7 @@ export class Plugin defaultMessage: 'Metrics', }), euiIconType: 'metricsApp', - order: 8001, + order: 8200, appRoute: '/app/metrics', category: DEFAULT_APP_CATEGORIES.observability, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 7c0c535795638..79c276a1e58f7 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -32,7 +32,7 @@ import { } from '../../../../../../../src/core/server'; import { RequestHandler } from '../../../../../../../src/core/server'; import { InfraConfig } from '../../../plugin'; -import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server'; +import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; export class KibanaFramework { public router: IRouter; @@ -197,10 +197,10 @@ export class KibanaFramework { ) { const { elasticsearch, uiSettings } = requestContext.core; - const includeFrozen = await uiSettings.client.get('search:includeFrozen'); + const includeFrozen = await uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); if (endpoint === 'msearch') { const maxConcurrentShardRequests = await uiSettings.client.get( - 'courier:maxConcurrentShardRequests' + UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS ); if (maxConcurrentShardRequests > 0) { params = { ...params, max_concurrent_shard_requests: maxConcurrentShardRequests }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 233a34a67d1ec..a282a742d614c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -336,6 +336,9 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s group, alertState: stateToAlertMessage[nextState], reason, + value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue), + threshold: mapToConditionsLookup(criteria, (c) => c.threshold), + metric: mapToConditionsLookup(criteria, (c) => c.metric), }); } @@ -352,3 +355,14 @@ export const FIRED_ACTIONS = { defaultMessage: 'Fired', }), }; + +const mapToConditionsLookup = ( + list: any[], + mapFn: (value: any, index: number, array: any[]) => unknown +) => + list + .map(mapFn) + .reduce( + (result: Record, value, i) => ({ ...result, [`condition${i}`]: value }), + {} + ); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 8b3903f2ee3be..2c98a568d16de 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -55,6 +55,30 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { } ); + const valueActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.valueActionVariableDescription', + { + defaultMessage: + 'The value of the metric in the specified condition. Usage: (ctx.value.condition0, ctx.value.condition1, etc...).', + } + ); + + const metricActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.metricActionVariableDescription', + { + defaultMessage: + 'The metric name in the specified condition. Usage: (ctx.metric.condition0, ctx.metric.condition1, etc...).', + } + ); + + const thresholdActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.thresholdActionVariableDescription', + { + defaultMessage: + 'The threshold value of the metric for the specified condition. Usage: (ctx.threshold.condition0, ctx.threshold.condition1, etc...).', + } + ); + return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: 'Metric threshold', @@ -82,6 +106,9 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { { name: 'group', description: groupActionVariableDescription }, { name: 'alertState', description: alertStateActionVariableDescription }, { name: 'reason', description: reasonActionVariableDescription }, + { name: 'value', description: valueActionVariableDescription }, + { name: 'metric', description: metricActionVariableDescription }, + { name: 'threshold', description: thresholdActionVariableDescription }, ], }, producer: 'metrics', diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index 924d12bec0c5c..d1a4ed431a2be 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -12,30 +12,48 @@ import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/invento import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field'; import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; import { ESSearchClient } from '.'; +import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; -export const createTimeRangeWithInterval = async ( - client: ESSearchClient, - options: InfraSnapshotRequestOptions -): Promise => { +const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequestOptions) => { + const { timerange } = options; + if (timerange.forceInterval && timerange.interval) { + return getIntervalInSeconds(timerange.interval); + } const aggregations = getMetricsAggregations(options); const modules = await aggregationsToModules(client, aggregations, options); - const interval = Math.max( + return Math.max( (await calculateMetricInterval( client, { indexPattern: options.sourceConfiguration.metricAlias, timestampField: options.sourceConfiguration.fields.timestamp, - timerange: { from: options.timerange.from, to: options.timerange.to }, + timerange: { from: timerange.from, to: timerange.to }, }, modules, options.nodeType )) || 60, 60 ); +}; + +export const createTimeRangeWithInterval = async ( + client: ESSearchClient, + options: InfraSnapshotRequestOptions +): Promise => { + const { timerange } = options; + const calculatedInterval = await createInterval(client, options); + if (timerange.ignoreLookback) { + return { + interval: `${calculatedInterval}s`, + from: timerange.from, + to: timerange.to, + }; + } + const lookbackSize = Math.max(timerange.lookbackSize || 5, 5); return { - interval: `${interval}s`, - from: options.timerange.to - interval * 5000, // We need at least 5 buckets worth of data - to: options.timerange.to, + interval: `${calculatedInterval}s`, + from: timerange.to - calculatedInterval * lookbackSize * 1000, // We need at least 5 buckets worth of data + to: timerange.to, }; }; diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts index 031eb881c91aa..6cb415d8e7ac4 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts @@ -7,6 +7,7 @@ import { isNumber, last, max, sum, get } from 'lodash'; import moment from 'moment'; +import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; import { InfraSnapshotRequestOptions } from './types'; import { findInventoryModel } from '../../../common/inventory_models'; @@ -127,12 +128,15 @@ export const getNodeMetrics = ( }; } const lastBucket = findLastFullBucket(nodeBuckets, options); - const result = { + const result: SnapshotNodeMetric = { name: options.metric.type, value: getMetricValueFromBucket(options.metric.type, lastBucket), max: calculateMax(nodeBuckets, options.metric.type), avg: calculateAvg(nodeBuckets, options.metric.type), }; + if (options.includeTimeseries) { + result.timeseries = getTimeseriesData(nodeBuckets, options.metric.type); + } return result; }; @@ -164,3 +168,20 @@ function calculateMax(buckets: InfraSnapshotMetricsBucket[], type: SnapshotMetri function calculateAvg(buckets: InfraSnapshotMetricsBucket[], type: SnapshotMetricType) { return sum(buckets.map((bucket) => getMetricValueFromBucket(type, bucket))) / buckets.length || 0; } + +function getTimeseriesData( + buckets: InfraSnapshotMetricsBucket[], + type: SnapshotMetricType +): MetricsExplorerSeries { + return { + id: type, + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: buckets.map((bucket) => ({ + timestamp: bucket.key as number, + metric_0: getMetricValueFromBucket(type, bucket), + })), + }; +} diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index 2d951d426b03a..3a0326fb6ae84 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -39,6 +39,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { timerange, accountId, region, + includeTimeseries, } = pipe( SnapshotRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) @@ -57,6 +58,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { sourceConfiguration: source.configuration, metric, timerange, + includeTimeseries, }; const searchES = ( diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index f0c2466b25b04..50c42544b8bdc 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -52,12 +52,12 @@ This plugin follows the `common`, `server`, `public` structure from the [Archite 1. In one terminal, change to the `x-pack` directory and start the test server with ``` - node scripts/functional_tests_server.js --config test/api_integration/config.js + node scripts/functional_tests_server.js --config test/api_integration/config.ts ``` 1. in a second terminal, run the tests from the Kibana root directory with ``` - node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.js + node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.ts ``` #### EPM diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 31a511f2d79dc..6ebfd3f28fd9b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -5,7 +5,7 @@ */ export { useCapabilities } from './use_capabilities'; -export { useCore, CoreContext } from './use_core'; +export { useCore } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; export { useBreadcrumbs } from './use_breadcrumbs'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts index f4e9a032b925a..9ce1e95aa91d5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; -import { CoreStart } from 'src/core/public'; +import { CoreStart } from 'kibana/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -export const CoreContext = React.createContext(null); - -export function useCore() { - const core = useContext(CoreContext); - if (core === null) { - throw new Error('CoreContext not initialized'); +export function useCore(): CoreStart { + const { services } = useKibana(); + if (services === null) { + throw new Error('KibanaContextProvider not initialized'); } - return core; + return services; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index f6a386314272f..ed5a75ce6c991 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -22,11 +22,12 @@ import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; -import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; +import { DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { useCore, sendSetup, sendGetPermissionsCheck } from './hooks'; import { FleetStatusProvider } from './hooks/use_fleet_status'; import './index.scss'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; export interface ProtectedRouteProps extends RouteProps { isAllowed?: boolean; @@ -229,7 +230,7 @@ const IngestManagerApp = ({ const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); return ( - + @@ -237,7 +238,7 @@ const IngestManagerApp = ({ - + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx index 30996931ba67a..73ddd567c515b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -50,6 +50,15 @@ export const agentConfigFormValidation = ( ]; } + if (!agentConfig.namespace?.trim()) { + errors.namespace = [ + , + ]; + } + return errors; }; @@ -73,7 +82,6 @@ export const AgentConfigForm: React.FunctionComponent = ({ onDelete = () => {}, }) => { const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); - const [showNamespace, setShowNamespace] = useState(!!agentConfig.namespace); const fields: Array<{ name: 'name' | 'description' | 'namespace'; label: JSX.Element; @@ -170,49 +178,28 @@ export const AgentConfigForm: React.FunctionComponent = ({ /> } > - - } - checked={showNamespace} - onChange={() => { - setShowNamespace(!showNamespace); - if (showNamespace) { - updateAgentConfig({ namespace: '' }); - } - }} - /> - {showNamespace && ( - <> - - - { - updateAgentConfig({ namespace: value }); - }} - onChange={(selectedOptions) => { - updateAgentConfig({ - namespace: (selectedOptions.length ? selectedOptions[0] : '') as string, - }); - }} - isInvalid={Boolean(touchedFields.namespace && validation.namespace)} - onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })} - /> - - - )} + + { + updateAgentConfig({ namespace: value }); + }} + onChange={(selectedOptions) => { + updateAgentConfig({ + namespace: (selectedOptions.length ? selectedOptions[0] : '') as string, + }); + }} + isInvalid={Boolean(touchedFields.namespace && validation.namespace)} + onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })} + /> + ; + +type AllowedDatasourceKey = 'endpoint'; +const ConfigureDatasourceMapping: { + [key: string]: CustomConfigureDatasourceContent; +} = {}; + +/** + * Plugins can call this function from the start lifecycle to + * register a custom component in the Ingest Datasource configuration. + */ +export function registerDatasource( + key: AllowedDatasourceKey, + value: CustomConfigureDatasourceContent +) { + ConfigureDatasourceMapping[key] = value; +} + +const EmptyConfigureDatasource: CustomConfigureDatasourceContent = () => ( + +

+ +

+ + } + /> +); + +export const CustomConfigureDatasource = (props: CustomConfigureDatasourceProps) => { + const ConfigureDatasourceContent = + ConfigureDatasourceMapping[props.packageName] || EmptyConfigureDatasource; + return ; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts index 3bfca75668911..42848cc0f5e41 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts @@ -6,3 +6,4 @@ export { CreateDatasourcePageLayout } from './layout'; export { DatasourceInputPanel } from './datasource_input_panel'; export { DatasourceInputVarField } from './datasource_input_var_field'; +export { CustomConfigureDatasource } from './custom_configure_datasource'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts index 992ace3530f40..67cde2dec3a56 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts @@ -106,6 +106,18 @@ describe('Ingest Manager - validateDatasource()', () => { { dataset: 'disabled2', input: 'disabled2', title: 'Disabled 2', enabled: false }, ], }, + { + type: 'with-no-stream-vars', + enabled: true, + vars: [{ required: true, name: 'var-name', type: 'text' }], + streams: [ + { + id: 'with-no-stream-vars-bar', + dataset: 'bar', + enabled: true, + }, + ], + }, ], }, ], @@ -172,12 +184,26 @@ describe('Ingest Manager - validateDatasource()', () => { vars: { 'var-name': { value: undefined, type: 'text' } }, }, { - id: 'with-disabled-streams-disabled2', + id: 'with-disabled-streams-disabled-without-vars', dataset: 'disabled2', enabled: false, }, ], }, + { + type: 'with-no-stream-vars', + enabled: true, + vars: { + 'var-name': { value: 'test', type: 'text' }, + }, + streams: [ + { + id: 'with-no-stream-vars-bar', + dataset: 'bar', + enabled: true, + }, + ], + }, ], }; @@ -245,12 +271,26 @@ describe('Ingest Manager - validateDatasource()', () => { }, }, { - id: 'with-disabled-streams-disabled2', + id: 'with-disabled-streams-disabled-without-vars', dataset: 'disabled2', enabled: false, }, ], }, + { + type: 'with-no-stream-vars', + enabled: true, + vars: { + 'var-name': { value: undefined, type: 'text' }, + }, + streams: [ + { + id: 'with-no-stream-vars-bar', + dataset: 'bar', + enabled: true, + }, + ], + }, ], }; @@ -274,7 +314,18 @@ describe('Ingest Manager - validateDatasource()', () => { }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, + streams: { + 'with-disabled-streams-disabled': { + vars: { 'var-name': null }, + }, + 'with-disabled-streams-disabled-without-vars': {}, + }, + }, + 'with-no-stream-vars': { + streams: { + 'with-no-stream-vars-bar': {}, + }, + vars: { 'var-name': null }, }, }, }; @@ -307,7 +358,16 @@ describe('Ingest Manager - validateDatasource()', () => { }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, + streams: { + 'with-disabled-streams-disabled': { vars: { 'var-name': null } }, + 'with-disabled-streams-disabled-without-vars': {}, + }, + }, + 'with-no-stream-vars': { + vars: { + 'var-name': ['var-name is required'], + }, + streams: { 'with-no-stream-vars-bar': {} }, }, }, }); @@ -354,7 +414,18 @@ describe('Ingest Manager - validateDatasource()', () => { }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, + streams: { + 'with-disabled-streams-disabled': { + vars: { 'var-name': null }, + }, + 'with-disabled-streams-disabled-without-vars': {}, + }, + }, + 'with-no-stream-vars': { + vars: { + 'var-name': ['var-name is required'], + }, + streams: { 'with-no-stream-vars-bar': {} }, }, }, }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts index 61273e1fb3db9..5b4cfe170a478 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts @@ -110,36 +110,31 @@ export const validateDatasource = ( // Validate each input stream with config fields if (input.streams.length) { input.streams.forEach((stream) => { - if (!stream.vars) { - return; - } - - const streamValidationResults: DatasourceConfigValidationResults = { - vars: undefined, - }; - - const streamVarsByName = ( - ( - registryInputsByType[input.type].streams.find( - (registryStream) => registryStream.dataset === stream.dataset - ) || {} - ).vars || [] - ).reduce((vars, registryVar) => { - vars[registryVar.name] = registryVar; - return vars; - }, {} as Record); + const streamValidationResults: DatasourceConfigValidationResults = {}; // Validate stream-level config fields - streamValidationResults.vars = Object.entries(stream.vars).reduce( - (results, [name, configEntry]) => { - results[name] = - input.enabled && stream.enabled - ? validateDatasourceConfig(configEntry, streamVarsByName[name]) - : null; - return results; - }, - {} as ValidationEntry - ); + if (stream.vars) { + const streamVarsByName = ( + ( + registryInputsByType[input.type].streams.find( + (registryStream) => registryStream.dataset === stream.dataset + ) || {} + ).vars || [] + ).reduce((vars, registryVar) => { + vars[registryVar.name] = registryVar; + return vars; + }, {} as Record); + streamValidationResults.vars = Object.entries(stream.vars).reduce( + (results, [name, configEntry]) => { + results[name] = + input.enabled && stream.enabled + ? validateDatasourceConfig(configEntry, streamVarsByName[name]) + : null; + return results; + }, + {} as ValidationEntry + ); + } inputValidationResults.streams![stream.id] = streamValidationResults; }); @@ -228,5 +223,6 @@ export const validationHasErrors = ( | DatasourceConfigValidationResults ) => { const flattenedValidation = getFlattenedObject(validationResults); + return !!Object.entries(flattenedValidation).find(([, value]) => !!value); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx index 58a98f86de426..d9cf0fbfb7987 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx @@ -5,28 +5,29 @@ */ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiEmptyPrompt, - EuiText, - EuiCallOut, -} from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { PackageInfo, NewDatasource, DatasourceInput } from '../../../types'; import { Loading } from '../../../components'; import { DatasourceValidationResults, validationHasErrors } from './services'; -import { DatasourceInputPanel } from './components'; +import { DatasourceInputPanel, CustomConfigureDatasource } from './components'; +import { CreateDatasourceFrom } from './types'; export const StepConfigureDatasource: React.FunctionComponent<{ + from?: CreateDatasourceFrom; packageInfo: PackageInfo; - datasource: NewDatasource; + datasource: NewDatasource | (NewDatasource & { id: string }); updateDatasource: (fields: Partial) => void; validationResults: DatasourceValidationResults; submitAttempted: boolean; -}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => { +}> = ({ + from = 'config', + packageInfo, + datasource, + updateDatasource, + validationResults, + submitAttempted, +}) => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; // Configure inputs (and their streams) @@ -68,19 +69,10 @@ export const StepConfigureDatasource: React.FunctionComponent<{ ) : ( - -

- -

- - } +
); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 1dd7e660deaa9..6fab78951038f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -169,6 +169,7 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { ))} ), + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [agentConfig, configId, agentStatus] ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx index 7be955bc9f4f3..4bb42faedf7f6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -69,7 +69,8 @@ export const EditDatasourcePage: React.FunctionComponent = () => { const [loadingError, setLoadingError] = useState(); const [agentConfig, setAgentConfig] = useState(); const [packageInfo, setPackageInfo] = useState(); - const [datasource, setDatasource] = useState({ + const [datasource, setDatasource] = useState({ + id: '', name: '', description: '', config_id: '', @@ -93,7 +94,6 @@ export const EditDatasourcePage: React.FunctionComponent = () => { } if (datasourceData?.item) { const { - id, revision, inputs, created_by, @@ -299,6 +299,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { ), children: ( (({ datastre items: [ { icon: 'dashboardApp', + /* eslint-disable-next-line react-hooks/rules-of-hooks */ href: useKibanaLink(`/dashboard/${dashboards[0].id || ''}`), name: actionNameSingular, }, @@ -70,6 +71,7 @@ export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastre items: dashboards.map((dashboard) => { return { icon: 'dashboardApp', + /* eslint-disable-next-line react-hooks/rules-of-hooks */ href: useKibanaLink(`/dashboard/${dashboard.id || ''}`), name: dashboard.title, }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx index 1a7681584ff15..5bb0464801f70 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx @@ -90,6 +90,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ), + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [agentData, agentId, getHref] ); @@ -141,6 +142,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ))} ) : undefined, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [agentConfigData, agentData, getHref, isAgentConfigLoading] ); diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index c11ad60dffee4..e26f310b6d9c6 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -11,3 +11,11 @@ export { IngestManagerStart } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; + +export { + CustomConfigureDatasourceContent, + CustomConfigureDatasourceProps, + registerDatasource, +} from './applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource'; + +export { NewDatasource } from './applications/ingest_manager/types'; diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index fd4e08f619495..3eb2fad339b7d 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -18,6 +18,7 @@ import { PLUGIN_ID } from '../common/constants'; import { IngestManagerConfigType } from '../common/types'; import { setupRouteService, appRoutesService } from '../common'; +import { registerDatasource } from './applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource'; export { IngestManagerConfigType } from '../common/types'; @@ -26,6 +27,7 @@ export type IngestManagerSetup = void; * Describes public IngestManager plugin contract returned at the `start` stage. */ export interface IngestManagerStart { + registerDatasource: typeof registerDatasource; success: boolean; error?: { message: string; @@ -80,12 +82,16 @@ export class IngestManagerPlugin const permissionsResponse = await core.http.get(appRoutesService.getCheckPermissionsPath()); if (permissionsResponse.success) { const { isInitialized: success } = await core.http.post(setupRouteService.getSetupPath()); - return { success }; + return { success, registerDatasource }; } else { throw new Error(permissionsResponse.error); } } catch (error) { - return { success: false, error: { message: error.body?.message || 'Unknown error' } }; + return { + success: false, + error: { message: error.body?.message || 'Unknown error' }, + registerDatasource, + }; } } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index 20b62eee9a317..5bbc376051122 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -30,6 +30,7 @@ export async function agentCheckin( const updateData: { last_checkin: string; default_api_key?: string; + default_api_key_id?: string; local_metadata?: AgentMetadata; current_error_events?: string; } = { @@ -51,11 +52,13 @@ export async function agentCheckin( // Assign output API keys // We currently only support default ouput if (!defaultApiKey) { - updateData.default_api_key = await APIKeysService.generateOutputApiKey( + const outputAPIKey = await APIKeysService.generateOutputApiKey( soClient, 'default', agent.id ); + updateData.default_api_key = outputAPIKey.key; + updateData.default_api_key_id = outputAPIKey.id; } // Mutate the config to set the api token for this agent config.outputs.default.api_key = defaultApiKey || updateData.default_api_key; diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts index 7d8d372a89ac4..6c95dc831aa9a 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts @@ -17,7 +17,7 @@ export async function generateOutputApiKey( soClient: SavedObjectsClientContract, outputId: string, agentId: string -): Promise { +): Promise<{ key: string; id: string }> { const name = `${agentId}:${outputId}`; const key = await createAPIKey(soClient, name, { 'fleet-output': { @@ -35,7 +35,7 @@ export async function generateOutputApiKey( throw new Error('Unable to create an output api key'); } - return `${key.id}:${key.api_key}`; + return { key: `${key.id}:${key.api_key}`, id: key.id }; } export async function generateAccessApiKey( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts index 5e83a976bd7a4..635dce93f0027 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -83,4 +83,22 @@ foo: bar custom: { foo: 'bar' }, }); }); + + it('should support optional yaml values at root level', () => { + const streamTemplate = ` +input: logs +{{custom}} + `; + const vars = { + custom: { + type: 'yaml', + value: null, + }, + }; + + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'logs', + }); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index 61f2f95fe20a9..0bcb2464f8d74 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -94,7 +94,10 @@ function replaceRootLevelYamlVariables(yamlVariables: { [k: string]: any }, yaml let patchedTemplate = yamlTemplate; Object.entries(yamlVariables).forEach(([key, val]) => { - patchedTemplate = patchedTemplate.replace(new RegExp(`^"${key}"`, 'gm'), safeDump(val)); + patchedTemplate = patchedTemplate.replace( + new RegExp(`^"${key}"`, 'gm'), + val ? safeDump(val) : '' + ); }); return patchedTemplate; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx index 3243d665832f2..fa8c4f82c1b68 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx @@ -5,13 +5,15 @@ */ /* eslint-disable @kbn/eslint/no-restricted-paths */ import React from 'react'; - +import { LocationDescriptorObject } from 'history'; +import { ScopedHistory } from 'kibana/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { notificationServiceMock, fatalErrorsServiceMock, docLinksServiceMock, injectedMetadataServiceMock, + scopedHistoryMock, } from '../../../../../../src/core/public/mocks'; import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/public/mocks'; @@ -33,12 +35,18 @@ const httpServiceSetupMock = new HttpService().setup({ fatalErrors: fatalErrorsServiceMock.createSetupContract(), }); +const history = (scopedHistoryMock.create() as unknown) as ScopedHistory; +history.createHref = (location: LocationDescriptorObject) => { + return `${location.pathname}?${location.search}`; +}; + const appServices = { breadcrumbs: breadcrumbService, metric: uiMetricService, documentation: documentationService, api: apiService, notifications: notificationServiceMock.createSetupContract(), + history, }; export const setupEnvironment = () => { diff --git a/x-pack/plugins/ingest_pipelines/common/constants.ts b/x-pack/plugins/ingest_pipelines/common/constants.ts index de291e364e02f..4c6c6fefaad83 100644 --- a/x-pack/plugins/ingest_pipelines/common/constants.ts +++ b/x-pack/plugins/ingest_pipelines/common/constants.ts @@ -11,7 +11,7 @@ export const PLUGIN_ID = 'ingest_pipelines'; export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; -export const BASE_PATH = '/management/ingest/ingest_pipelines'; +export const BASE_PATH = '/'; export const API_BASE_PATH = '/api/ingest_pipelines'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index f4ac640722120..55b59caab8d60 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -6,9 +6,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageContent } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; -import { HashRouter, Switch, Route } from 'react-router-dom'; +import { Router, Switch, Route } from 'react-router-dom'; -import { BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../common/constants'; +import { useKibana } from '../shared_imports'; + +import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../common/constants'; import { SectionError, @@ -22,10 +24,10 @@ import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from '. export const AppWithoutRouter = () => ( - - - - + + + + {/* Catch all */} @@ -33,6 +35,7 @@ export const AppWithoutRouter = () => ( export const App: FunctionComponent = () => { const { apiError } = useAuthorizationContext(); + const { history } = useKibana().services; if (apiError) { return ( @@ -91,9 +94,9 @@ export const App: FunctionComponent = () => { } return ( - + - + ); }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx index e43dba4689b44..a8e6febeb2e59 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -8,6 +8,7 @@ import { HttpSetup } from 'kibana/public'; import React, { ReactNode } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { NotificationsSetup } from 'kibana/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { API_BASE_PATH } from '../../common/constants'; @@ -23,6 +24,7 @@ export interface AppServices { documentation: DocumentationService; api: ApiService; notifications: NotificationsSetup; + history: ManagementAppMountParams['history']; } export interface CoreServices { diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts index e36f27cbf5f62..49c8f5a7b2e1e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -13,7 +13,7 @@ export async function mountManagementSection( { http, getStartServices, notifications }: CoreSetup, params: ManagementAppMountParams ) { - const { element, setBreadcrumbs } = params; + const { element, setBreadcrumbs, history } = params; const [coreStart] = await getStartServices(); const { docLinks, @@ -29,6 +29,7 @@ export async function mountManagementSection( documentation: documentationService, api: apiService, notifications, + history, }; return renderApp(element, I18nContext, services, { http }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index f6fe2f0cf65fa..eba69ff454911 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -6,12 +6,15 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiEmptyPrompt, EuiLink, EuiPageBody, EuiPageContent } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; +import { EuiEmptyPrompt, EuiLink, EuiPageBody, EuiPageContent, EuiButton } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { ScopedHistory } from 'kibana/public'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { useKibana } from '../../../shared_imports'; export const EmptyList: FunctionComponent = () => { const { services } = useKibana(); + const history = useHistory() as ScopedHistory; return ( @@ -41,7 +44,7 @@ export const EmptyList: FunctionComponent = () => {

} actions={ - + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { defaultMessage: 'Create a pipeline', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 541a2b486b5a7..97775965f9b45 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -13,9 +13,10 @@ import { EuiInMemoryTableProps, EuiTableFieldDataColumnType, } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; -import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; +import { useKibana } from '../../../shared_imports'; export interface Props { pipelines: Pipeline[]; @@ -32,6 +33,7 @@ export const PipelineTable: FunctionComponent = ({ onClonePipelineClick, onDeletePipelineClick, }) => { + const { history } = useKibana().services; const [selection, setSelection] = useState([]); const tableProps: EuiInMemoryTableProps = { @@ -80,14 +82,14 @@ export const PipelineTable: FunctionComponent = ({ })} , {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { - defaultMessage: 'Create a pipeline', + defaultMessage: 'Create a pipeline here', })} , ], @@ -107,7 +109,10 @@ export const PipelineTable: FunctionComponent = ({ }), sortable: true, render: (name: string) => ( - + {name} ), diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts index 1ccdbbad9b1bb..5fc8e13e3dcad 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { BASE_PATH } from '../../../common/constants'; import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; @@ -28,7 +27,7 @@ export class BreadcrumbService { create: [ { text: homeBreadcrumbText, - href: `#${BASE_PATH}`, + href: `/`, }, { text: i18n.translate('xpack.ingestPipelines.breadcrumb.createPipelineLabel', { @@ -39,7 +38,7 @@ export class BreadcrumbService { edit: [ { text: homeBreadcrumbText, - href: `#${BASE_PATH}`, + href: `/`, }, { text: i18n.translate('xpack.ingestPipelines.breadcrumb.editPipelineLabel', { diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index f1a2edd2d554f..33f4f46681a28 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -19,6 +19,7 @@ import { FilterManager, IFieldType, IIndexPattern, + UI_SETTINGS, } from '../../../../../src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const dataStartMock = dataPluginMock.createStartContract(); @@ -183,7 +184,7 @@ describe('Lens App', () => { jest.fn((type) => { if (type === 'timepicker:timeDefaults') { return { from: 'now-7d', to: 'now' }; - } else if (type === 'search:queryLanguage') { + } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { return 'kuery'; } else { return []; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index ffa59a6fb6bc9..1349d33983fcd 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -27,6 +27,7 @@ import { IndexPattern as IndexPatternInstance, IndexPatternsContract, SavedQuery, + UI_SETTINGS, } from '../../../../../src/plugins/data/public'; interface State { @@ -76,7 +77,8 @@ export function App({ onAppLeave: AppMountParameters['onAppLeave']; }) { const language = - storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); + storage.get('kibana.userQueryLanguage') || + core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE); const [state, setState] = useState(() => { const currentRange = data.query.timefilter.timefilter.getTime(); @@ -413,7 +415,7 @@ export function App({ query: '', language: storage.get('kibana.userQueryLanguage') || - core.uiSettings.get('search:queryLanguage'), + core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }, })); }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index e665e8b8dd326..defc142d4976e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -12,6 +12,7 @@ import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { coreMock } from 'src/core/public/mocks'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; import { dataPluginMock, getCalculateAutoTimeExpression, @@ -23,7 +24,7 @@ const dataStart = dataPluginMock.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression({ ...coreMock.createStart().uiSettings, get: (path: string) => { - if (path === 'histogram:maxBars') { + if (path === UI_SETTINGS.HISTOGRAM_MAX_BARS) { return 10; } }, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index f9a577e001c64..3000c9321b3b9 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -6,7 +6,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; import { VisualizationsSetup } from 'src/plugins/visualizations/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; @@ -38,7 +38,6 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; - embeddable: EmbeddableStart; expressions: ExpressionsStart; navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 23cf9e7ff818f..cd25cb5729511 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -8,6 +8,7 @@ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist import { CoreSetup, IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; @@ -47,7 +48,7 @@ export class XyVisualization { ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, timeZone: getTimeZone(core.uiSettings), - histogramBarTarget: core.uiSettings.get('histogram:barTarget'), + histogramBarTarget: core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), }) ); diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md index 60b4266edadb3..70d7f16b0f7fc 100644 --- a/x-pack/plugins/lens/readme.md +++ b/x-pack/plugins/lens/readme.md @@ -11,4 +11,4 @@ Run all tests from the `x-pack` root directory - You may want to comment out all imports except for Lens in the config file. - API Functional tests: - Run `node scripts/functional_tests_server` - - Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.js --grep=Lens` + - Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.ts --grep=Lens` diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap index e4411807dfa56..28ce3c6c07501 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; -exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index b621e89efbee3..cc8cbfe679eff 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -112,6 +112,42 @@ exports[`UploadLicense should display a modal when license requires acknowledgem "refresh": [MockFunction], }, }, + "services": Object { + "history": Object { + "action": "PUSH", + "block": [MockFunction], + "createHref": [MockFunction] { + "calls": Array [ + Array [ + Object { + "pathname": "/home", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": "/home", + }, + ], + }, + "createSubHistory": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "length": 1, + "listen": [MockFunction], + "location": Object { + "hash": "", + "key": undefined, + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [MockFunction], + "replace": [MockFunction], + }, + }, } } > @@ -126,12 +162,85 @@ exports[`UploadLicense should display a modal when license requires acknowledgem } } > - + @@ -1162,12 +1336,139 @@ exports[`UploadLicense should display an error when ES says license is expired 1 } } > - + @@ -1628,12 +1994,139 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 } } > - + @@ -2094,12 +2652,139 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] } } > - + @@ -2560,12 +3310,139 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` } } > - + {}; let store: any = null; let component: any = null; +const history = scopedHistoryMock.create(); +history.createHref.mockImplementation((location: LocationDescriptorObject) => { + return `${location.pathname}${location.search ? '?' + location.search : ''}`; +}); const appDependencies = { plugins: { @@ -39,14 +44,15 @@ const appDependencies = { refresh: jest.fn(), }, }, + services: { + history, + }, docLinks: {}, }; const thunkServices = { http: httpServiceMock.createSetupContract(), - history: { - replace: jest.fn(), - }, + history, breadcrumbService: { setBreadcrumbs() {}, }, @@ -59,7 +65,7 @@ describe('UploadLicense', () => { component = ( - + ); diff --git a/x-pack/plugins/license_management/__jest__/util/util.js b/x-pack/plugins/license_management/__jest__/util/util.js index 5a7e49c8c3315..c13dcdb7fdbfa 100644 --- a/x-pack/plugins/license_management/__jest__/util/util.js +++ b/x-pack/plugins/license_management/__jest__/util/util.js @@ -9,14 +9,22 @@ import React from 'react'; import { Provider } from 'react-redux'; import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; -import { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { httpServiceMock, scopedHistoryMock } from '../../../../../src/core/public/mocks'; import { licenseManagementStore } from '../../public/application/store/store'; import { AppContextProvider } from '../../public/application/app_context'; const highExpirationMillis = new Date('October 13, 2099 00:00:00Z').getTime(); +const history = scopedHistoryMock.create(); +history.createHref.mockImplementation((location) => { + return `${location.pathname}${location.search ? '?' + location.search : ''}`; +}); + const appDependencies = { docLinks: {}, + services: { + history, + }, }; export const createMockLicense = (type, expiryDateInMillis = highExpirationMillis) => { @@ -30,6 +38,7 @@ export const createMockLicense = (type, expiryDateInMillis = highExpirationMilli export const getComponent = (initialState, Component) => { const services = { http: httpServiceMock.createSetupContract(), + history, }; const store = licenseManagementStore(initialState, services); return mountWithIntl( diff --git a/x-pack/plugins/license_management/common/constants/base_path.ts b/x-pack/plugins/license_management/common/constants/base_path.ts index 6ed03a0428096..ffb470e931921 100644 --- a/x-pack/plugins/license_management/common/constants/base_path.ts +++ b/x-pack/plugins/license_management/common/constants/base_path.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const BASE_PATH = '/management/stack/license_management/'; - export const API_BASE_PATH = '/api/license'; diff --git a/x-pack/plugins/license_management/common/constants/index.ts b/x-pack/plugins/license_management/common/constants/index.ts index ec411fea4b7a9..a531ba08401f8 100644 --- a/x-pack/plugins/license_management/common/constants/index.ts +++ b/x-pack/plugins/license_management/common/constants/index.ts @@ -5,6 +5,6 @@ */ export { PLUGIN } from './plugin'; -export { BASE_PATH, API_BASE_PATH } from './base_path'; +export { API_BASE_PATH } from './base_path'; export { EXTERNAL_LINKS } from './external_links'; export { APP_PERMISSION } from './permissions'; diff --git a/x-pack/plugins/license_management/public/application/app.js b/x-pack/plugins/license_management/public/application/app.js index 46d0da5252ceb..6885a249be01c 100644 --- a/x-pack/plugins/license_management/public/application/app.js +++ b/x-pack/plugins/license_management/public/application/app.js @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { LicenseDashboard, UploadLicense } from './sections'; import { Switch, Route } from 'react-router-dom'; -import { APP_PERMISSION, BASE_PATH } from '../../common/constants'; +import { APP_PERMISSION } from '../../common/constants'; import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui'; export class App extends Component { @@ -89,8 +89,8 @@ export class App extends Component { return ( - - + + ); diff --git a/x-pack/plugins/license_management/public/application/app_context.tsx b/x-pack/plugins/license_management/public/application/app_context.tsx index 1e90f4c907b8c..39e7ef5f16e79 100644 --- a/x-pack/plugins/license_management/public/application/app_context.tsx +++ b/x-pack/plugins/license_management/public/application/app_context.tsx @@ -5,6 +5,7 @@ */ import React, { createContext, useContext } from 'react'; +import { ScopedHistory } from 'kibana/public'; import { CoreStart } from '../../../../../src/core/public'; import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; @@ -18,6 +19,7 @@ export interface AppDependencies { core: CoreStart; services: { breadcrumbService: BreadcrumbService; + history: ScopedHistory; }; plugins: { licensing: LicensingPluginSetup; diff --git a/x-pack/plugins/license_management/public/application/app_providers.tsx b/x-pack/plugins/license_management/public/application/app_providers.tsx index 9f9fd2a8275df..139290f2c46ce 100644 --- a/x-pack/plugins/license_management/public/application/app_providers.tsx +++ b/x-pack/plugins/license_management/public/application/app_providers.tsx @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import * as history from 'history'; import { Provider } from 'react-redux'; -import { BASE_PATH } from '../../common/constants'; import { AppContextProvider, AppDependencies } from './app_context'; // @ts-ignore import { licenseManagementStore } from './store'; @@ -33,8 +31,7 @@ export const AppProviders = ({ appDependencies, children }: Props) => { // Setup Redux store const thunkServices = { - // So we can imperatively control the hash route - history: history.createHashHistory({ basename: BASE_PATH }), + history: appDependencies.services.history, toasts, http, telemetry: plugins.telemetry, diff --git a/x-pack/plugins/license_management/public/application/breadcrumbs.ts b/x-pack/plugins/license_management/public/application/breadcrumbs.ts index b1773a10f01ba..d3a69f55c4347 100644 --- a/x-pack/plugins/license_management/public/application/breadcrumbs.ts +++ b/x-pack/plugins/license_management/public/application/breadcrumbs.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; -import { BASE_PATH } from '../../common/constants'; type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; @@ -32,7 +31,7 @@ export class BreadcrumbService { text: i18n.translate('xpack.licenseMgmt.dashboard.breadcrumb', { defaultMessage: 'License management', }), - href: `#${BASE_PATH}home`, + href: `/`, }, ]; diff --git a/x-pack/plugins/license_management/public/application/index.tsx b/x-pack/plugins/license_management/public/application/index.tsx index 75f2f98f51e6e..cca164b14b8b8 100644 --- a/x-pack/plugins/license_management/public/application/index.tsx +++ b/x-pack/plugins/license_management/public/application/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HashRouter } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { AppDependencies } from './app_context'; import { AppProviders } from './app_providers'; @@ -14,15 +14,18 @@ import { AppProviders } from './app_providers'; import { App } from './app.container'; const AppWithRouter = (props: { [key: string]: any }) => ( - + - + ); export const renderApp = (element: Element, dependencies: AppDependencies) => { render( - + , element ); diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js index 158702e1286ae..d13a3bc34a7f2 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js @@ -5,12 +5,16 @@ */ import React from 'react'; -import { BASE_PATH } from '../../../../../common/constants'; import { EuiCard, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useAppContext } from '../../../app_context'; + +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; + +export const AddLicense = ({ uploadPath = `/upload_license` }) => { + const { services } = useAppContext(); -export const AddLicense = ({ uploadPath = `#${BASE_PATH}upload_license` }) => { return ( { /> } footer={ - + @@ -189,7 +190,7 @@ export class UploadLicense extends React.PureComponent { - + { + mount: async ({ element, setBreadcrumbs, history }) => { const [core] = await getStartServices(); const initialLicense = await plugins.licensing.license$.pipe(first()).toPromise(); @@ -72,6 +72,7 @@ export class LicenseManagementUIPlugin }, services: { breadcrumbService: this.breadcrumbService, + history, }, store: { initialLicense, diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index 564f2fdb21116..960fe3699e210 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -374,7 +374,7 @@ describe('licensing plugin', () => { expect(coreStart.overlays.banners.add).toHaveBeenCalledTimes(1); expect(mountExpiredBannerMock).toHaveBeenCalledWith({ type: 'gold', - uploadUrl: '/app/kibana#/management/stack/license_management/upload_license', + uploadUrl: '/app/management/stack/license_management/upload_license', }); }); }); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index c39acb12b06e1..ec42a73f610c0 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -161,7 +161,7 @@ export class LicensingPlugin implements Plugin new LicensingPlugin(context); export * from '../common/types'; +export { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; export * from './types'; export { config } from './licensing_config'; export { CheckLicense, wrapRouteWithLicenseCheck } from './wrap_route_with_license_check'; diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts index eb4b393a36da3..9de40e3f72932 100644 --- a/x-pack/plugins/lists/common/siem_common_deps.ts +++ b/x-pack/plugins/lists/common/siem_common_deps.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { exactCheck } from '../../siem/common/exact_check'; -export { getPaths, foldLeftRight } from '../../siem/common/test_utils'; +export { exactCheck } from '../../security_solution/common/exact_check'; +export { getPaths, foldLeftRight } from '../../security_solution/common/test_utils'; diff --git a/x-pack/plugins/lists/server/siem_server_deps.ts b/x-pack/plugins/lists/server/siem_server_deps.ts index 81ebe9f17b06f..df4b07fc46322 100644 --- a/x-pack/plugins/lists/server/siem_server_deps.ts +++ b/x-pack/plugins/lists/server/siem_server_deps.ts @@ -18,4 +18,4 @@ export { getIndexExists, buildRouteValidation, validate, -} from '../../siem/server'; +} from '../../security_solution/server'; diff --git a/x-pack/plugins/logstash/public/application/index.tsx b/x-pack/plugins/logstash/public/application/index.tsx index 3588e1f6b2417..8d515ad6b3932 100644 --- a/x-pack/plugins/logstash/public/application/index.tsx +++ b/x-pack/plugins/logstash/public/application/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'; +import { Router, Route, Switch, Redirect } from 'react-router-dom'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; @@ -30,7 +30,7 @@ import * as Breadcrumbs from './breadcrumbs'; export const renderApp = async ( core: CoreStart, - { basePath, element, setBreadcrumbs }: ManagementAppMountParams, + { history, element, setBreadcrumbs }: ManagementAppMountParams, isMonitoringEnabled: boolean, licenseService$: Observable ) => { @@ -43,12 +43,12 @@ export const renderApp = async ( ReactDOM.render( - + { + render={() => { setBreadcrumbs(Breadcrumbs.getPipelineListBreadcrumbs()); return ( history.push(`/pipeline/${id}/edit`)} clonePipeline={(id: string) => history.push(`/pipeline/${id}/edit?clone`)} - createPipeline={() => history.push(`/pipeline/new-pipeline`)} + createPipeline={() => history.push(`pipeline/new-pipeline`)} pipelinesService={pipelinesService} toastNotifications={core.notifications.toasts} /> @@ -70,7 +70,7 @@ export const renderApp = async ( ( + render={() => ( ( + render={({ match }) => ( - + , element ); diff --git a/x-pack/plugins/logstash/public/plugin.ts b/x-pack/plugins/logstash/public/plugin.ts index 70fdb420ca2d2..ade6abdb63f43 100644 --- a/x-pack/plugins/logstash/public/plugin.ts +++ b/x-pack/plugins/logstash/public/plugin.ts @@ -71,7 +71,7 @@ export class LogstashPlugin implements Plugin { defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', }), icon: 'pipelineApp', - path: '/app/kibana#/management/ingest/pipelines', + path: '/app/management/ingest/pipelines', showOnHomePage: true, category: FeatureCatalogueCategory.ADMIN, }); diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts index 4f61d7501f977..c7bfe94742bd6 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts @@ -7,7 +7,7 @@ import { RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; import { MapExtent, MapQuery } from './map_descriptor'; -import { Filter, TimeRange } from '../../../../../src/plugins/data/public'; +import { Filter, TimeRange } from '../../../../../src/plugins/data/common'; // Global map state passed to every layer. export type MapFilters = { diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index 798b5f335dda2..00380ca12a486 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { Query } from '../../../../../src/plugins/data/public'; +import { Query } from '../../../../../src/plugins/data/common'; import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; export type MapExtent = { diff --git a/x-pack/plugins/maps/public/angular/get_initial_query.js b/x-pack/plugins/maps/public/angular/get_initial_query.js index 4f61142413671..84f431cf2b3b6 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_query.js +++ b/x-pack/plugins/maps/public/angular/get_initial_query.js @@ -5,6 +5,7 @@ */ import { getUiSettings } from '../kibana_services'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage }) { const settings = getUiSettings(); @@ -22,6 +23,6 @@ export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage return { query: '', - language: userQueryLanguage || settings.get('search:queryLanguage'), + language: userQueryLanguage || settings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }; } diff --git a/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js index f13e435cd1d5c..17a50c6c5f685 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js +++ b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js @@ -5,6 +5,7 @@ */ import { getUiSettings } from '../kibana_services'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { const uiSettings = getUiSettings(); @@ -16,7 +17,7 @@ export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { } } - const defaultRefreshConfig = uiSettings.get('timepicker:refreshIntervalDefaults'); + const defaultRefreshConfig = uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); const refreshInterval = { ...defaultRefreshConfig, ...globalState.refreshInterval }; return { isPaused: refreshInterval.pause, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts index a8fba834d65ab..f4ef9bdbe5b6d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts @@ -25,6 +25,21 @@ class MockField extends AbstractField { } } +export class MockMbMap { + _paintPropertyCalls: unknown[]; + + constructor() { + this._paintPropertyCalls = []; + } + setPaintProperty(...args: unknown[]) { + this._paintPropertyCalls.push([...args]); + } + + getPaintPropertyCalls(): unknown[] { + return this._paintPropertyCalls; + } +} + export const mockField: IField = new MockField({ fieldName: 'foobar', origin: FIELD_ORIGIN.SOURCE, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js index 898da439c44aa..a0af2fbb939d8 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js @@ -110,6 +110,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty { _getMbDataDrivenSize({ targetName, minSize, maxSize, minValue, maxValue }) { const lookup = this.supportsMbFeatureState() ? 'feature-state' : 'get'; + + const stops = + minValue === maxValue ? [maxValue, maxSize] : [minValue, minSize, maxValue, maxSize]; return [ 'interpolate', ['linear'], @@ -120,10 +123,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { fieldName: targetName, fallback: 0, }), - minValue, - minSize, - maxValue, - maxSize, + ...stops, ]; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx index 34f3e796f409f..c60547f3606c5 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IVectorStyle } from '../vector_style'; - jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { @@ -18,68 +16,22 @@ import { shallow } from 'enzyme'; // @ts-ignore import { DynamicSizeProperty } from './dynamic_size_property'; -import { StyleMeta } from '../style_meta'; -import { FIELD_ORIGIN, VECTOR_STYLES } from '../../../../../common/constants'; -import { DataRequest } from '../../../util/data_request'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { VECTOR_STYLES } from '../../../../../common/constants'; import { IField } from '../../../fields/field'; +import { MockMbMap } from './__tests__/test_util'; -// @ts-ignore -const mockField: IField = { - async getLabel() { - return 'foobar_label'; - }, - getName() { - return 'foobar'; - }, - getRootName() { - return 'foobar'; - }, - getOrigin() { - return FIELD_ORIGIN.SOURCE; - }, - supportsFieldMeta() { - return true; - }, - canValueBeFormatted() { - return true; - }, - async getDataType() { - return 'number'; - }, -}; +import { mockField, MockLayer, MockStyle } from './__tests__/test_util'; -// @ts-ignore -const mockLayer: IVectorLayer = { - getDataRequest(): DataRequest | undefined { - return undefined; - }, - getStyle(): IVectorStyle { - // @ts-ignore - return { - getStyleMeta(): StyleMeta { - return new StyleMeta({ - geometryTypes: { - isPointsOnly: true, - isLinesOnly: false, - isPolygonsOnly: false, - }, - fieldMeta: { - foobar: { - range: { min: 0, max: 100, delta: 100 }, - categories: { categories: [] }, - }, - }, - }); - }, - }; - }, -}; - -const makeProperty: DynamicSizeProperty = (options: object) => { - return new DynamicSizeProperty(options, VECTOR_STYLES.ICON_SIZE, mockField, mockLayer, () => { - return (x: string) => x + '_format'; - }); +const makeProperty = (options: object, mockStyle: MockStyle, field: IField = mockField) => { + return new DynamicSizeProperty( + options, + VECTOR_STYLES.ICON_SIZE, + field, + new MockLayer(mockStyle), + () => { + return (x: string) => x + '_format'; + } + ); }; const defaultLegendParams = { @@ -89,7 +41,7 @@ const defaultLegendParams = { describe('renderLegendDetailRow', () => { test('Should render as range', async () => { - const sizeProp = makeProperty(); + const sizeProp = makeProperty({}, new MockStyle({ min: 0, max: 100 })); const legendRow = sizeProp.renderLegendDetailRow(defaultLegendParams); const component = shallow(legendRow); @@ -100,3 +52,70 @@ describe('renderLegendDetailRow', () => { expect(component).toMatchSnapshot(); }); }); + +describe('syncSize', () => { + test('Should sync with circle-radius prop', async () => { + const sizeProp = makeProperty({ minSize: 8, maxSize: 32 }, new MockStyle({ min: 0, max: 100 })); + const mockMbMap = new MockMbMap(); + + sizeProp.syncCircleRadiusWithMb('foobar', mockMbMap); + + expect(mockMbMap.getPaintPropertyCalls()).toEqual([ + [ + 'foobar', + 'circle-radius', + [ + 'interpolate', + ['linear'], + [ + 'coalesce', + [ + 'case', + ['==', ['feature-state', 'foobar'], null], + -1, + ['max', ['min', ['to-number', ['feature-state', 'foobar']], 100], 0], + ], + 0, + ], + 0, + 8, + 100, + 32, + ], + ], + ]); + }); + + test('Should truncate interpolate expression to max when no delta', async () => { + const sizeProp = makeProperty( + { minSize: 8, maxSize: 32 }, + new MockStyle({ min: 100, max: 100 }) + ); + const mockMbMap = new MockMbMap(); + + sizeProp.syncCircleRadiusWithMb('foobar', mockMbMap); + + expect(mockMbMap.getPaintPropertyCalls()).toEqual([ + [ + 'foobar', + 'circle-radius', + [ + 'interpolate', + ['linear'], + [ + 'coalesce', + [ + 'case', + ['==', ['feature-state', 'foobar'], null], + 99, + ['max', ['min', ['to-number', ['feature-state', 'foobar']], 100], 100], + ], + 0, + ], + 100, + 32, + ], + ], + ]); + }); +}); diff --git a/x-pack/plugins/maps/public/components/no_index_pattern_callout.js b/x-pack/plugins/maps/public/components/no_index_pattern_callout.js index 89cd884a6dd32..2daab8c6322dd 100644 --- a/x-pack/plugins/maps/public/components/no_index_pattern_callout.js +++ b/x-pack/plugins/maps/public/components/no_index_pattern_callout.js @@ -24,7 +24,7 @@ export function NoIndexPatternCallout() { id="xpack.maps.noIndexPattern.doThisPrefixDescription" defaultMessage="You'll need to " /> - + { const el = document.querySelector(`[data-dom-id="${this.state.domId}"]`); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 75b6b5d66f1de..45c7507160e98 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -20,6 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; import { getIndexPatternService, getUiSettings, getData } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; @@ -101,7 +102,7 @@ export class FilterEditor extends Component { query={ layerQuery ? layerQuery - : { language: uiSettings.get('search:queryLanguage'), query: '' } + : { language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '' } } onQuerySubmit={this._onQueryChange} indexPatterns={this.state.indexPatterns} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index d87761d3dabe8..8fdb71de2dfed 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -8,6 +8,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiExpression, EuiFormHelpText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { getUiSettings, getData } from '../../../../kibana_services'; export class WhereExpression extends Component { @@ -79,7 +80,7 @@ export class WhereExpression extends Component { query={ whereQuery ? whereQuery - : { language: getUiSettings().get('search:queryLanguage'), query: '' } + : { language: getUiSettings().get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '' } } onQuerySubmit={this._onQueryChange} indexPatterns={[indexPattern]} diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts index bc55c7549c589..3dbdb8bf3c002 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts @@ -50,6 +50,7 @@ export interface AnalysisConfig { latency?: number; multivariate_by_fields?: boolean; summary_count_field_name?: string; + per_partition_categorization?: PerPartitionCategorization; } export interface Detector { @@ -77,6 +78,7 @@ export interface DataDescription { export interface ModelPlotConfig { enabled: boolean; + annotations_enabled?: boolean; terms?: string; } @@ -86,3 +88,8 @@ export interface CustomRule { scope?: object; conditions: any[]; } + +export interface PerPartitionCategorization { + enabled: boolean; + stop_on_warn?: boolean; +} diff --git a/x-pack/plugins/ml/common/types/categories.ts b/x-pack/plugins/ml/common/types/categories.ts index 5d4c3eab53ee8..b3655f274b362 100644 --- a/x-pack/plugins/ml/common/types/categories.ts +++ b/x-pack/plugins/ml/common/types/categories.ts @@ -16,6 +16,8 @@ export interface Category { max_matching_length: number; examples: string[]; grok_pattern: string; + partition_field_name?: string; // TODO: make non-optional once fields have been added to the results + partition_field_value?: string; // TODO: make non-optional once fields have been added to the results } export interface Token { diff --git a/x-pack/plugins/ml/common/util/validators.ts b/x-pack/plugins/ml/common/util/validators.ts index 6f959c50219cf..5dcdec0553106 100644 --- a/x-pack/plugins/ml/common/util/validators.ts +++ b/x-pack/plugins/ml/common/util/validators.ts @@ -65,6 +65,8 @@ export function requiredValidator() { }; } +export type ValidationResult = object | null; + export function memoryInputValidator(allowedUnits = ALLOWED_DATA_UNITS) { return (value: any) => { if (typeof value !== 'string' || value === '') { diff --git a/x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js similarity index 84% rename from x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js rename to x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js index af282d53273d1..c86b716b2f49b 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js +++ b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js @@ -29,7 +29,7 @@ import { import { Pager } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; -const JOBS_PER_PAGE = 20; +const ITEMS_PER_PAGE = 20; function getError(error) { if (error !== null) { @@ -43,15 +43,18 @@ function getError(error) { } export function CustomSelectionTable({ + checkboxDisabledCheck, columns, filterDefaultFields, filters, items, + itemsPerPage = ITEMS_PER_PAGE, onTableChange, + radioDisabledCheck, selectedIds, singleSelection, sortableProperties, - timeseriesOnly, + tableItemId = 'id', }) { const [itemIdToSelectedMap, setItemIdToSelectedMap] = useState(getCurrentlySelectedItemIdsMap()); const [currentItems, setCurrentItems] = useState(items); @@ -59,7 +62,7 @@ export function CustomSelectionTable({ const [sortedColumn, setSortedColumn] = useState(''); const [pager, setPager] = useState(); const [pagerSettings, setPagerSettings] = useState({ - itemsPerPage: JOBS_PER_PAGE, + itemsPerPage: itemsPerPage, firstItemIndex: 0, lastItemIndex: 1, }); @@ -77,9 +80,9 @@ export function CustomSelectionTable({ }, [selectedIds]); // eslint-disable-line useEffect(() => { - const tablePager = new Pager(currentItems.length, JOBS_PER_PAGE); + const tablePager = new Pager(currentItems.length, itemsPerPage); setPagerSettings({ - itemsPerPage: JOBS_PER_PAGE, + itemsPerPage: itemsPerPage, firstItemIndex: tablePager.getFirstItemIndex(), lastItemIndex: tablePager.getLastItemIndex(), }); @@ -100,7 +103,7 @@ export function CustomSelectionTable({ function handleTableChange({ isSelected, itemId }) { const selectedMapIds = Object.getOwnPropertyNames(itemIdToSelectedMap); - const currentItemIds = currentItems.map((item) => item.id); + const currentItemIds = currentItems.map((item) => item[tableItemId]); let currentSelected = selectedMapIds.filter( (id) => itemIdToSelectedMap[id] === true && id !== itemId @@ -124,11 +127,11 @@ export function CustomSelectionTable({ onTableChange(currentSelected); } - function handleChangeItemsPerPage(itemsPerPage) { - pager.setItemsPerPage(itemsPerPage); + function handleChangeItemsPerPage(numItemsPerPage) { + pager.setItemsPerPage(numItemsPerPage); setPagerSettings({ ...pagerSettings, - itemsPerPage, + itemsPerPage: numItemsPerPage, firstItemIndex: pager.getFirstItemIndex(), lastItemIndex: pager.getLastItemIndex(), }); @@ -161,7 +164,9 @@ export function CustomSelectionTable({ } function areAllItemsSelected() { - const indexOfUnselectedItem = currentItems.findIndex((item) => !isItemSelected(item.id)); + const indexOfUnselectedItem = currentItems.findIndex( + (item) => !isItemSelected(item[tableItemId]) + ); return indexOfUnselectedItem === -1; } @@ -199,7 +204,7 @@ export function CustomSelectionTable({ function toggleAll() { const allSelected = areAllItemsSelected() || itemIdToSelectedMap.all === true; const newItemIdToSelectedMap = {}; - currentItems.forEach((item) => (newItemIdToSelectedMap[item.id] = !allSelected)); + currentItems.forEach((item) => (newItemIdToSelectedMap[item[tableItemId]] = !allSelected)); setItemIdToSelectedMap(newItemIdToSelectedMap); handleTableChange({ isSelected: !allSelected, itemId: 'all' }); } @@ -255,20 +260,23 @@ export function CustomSelectionTable({ {!singleSelection && ( toggleItem(item.id)} + disabled={ + checkboxDisabledCheck !== undefined ? checkboxDisabledCheck(item) : undefined + } + id={`${item[tableItemId]}-checkbox`} + data-test-subj={`${item[tableItemId]}-checkbox`} + checked={isItemSelected(item[tableItemId])} + onChange={() => toggleItem(item[tableItemId])} type="inList" /> )} {singleSelection && ( toggleItem(item.id)} - disabled={timeseriesOnly && item.isSingleMetricViewerJob === false} + id={item[tableItemId]} + data-test-subj={`${item[tableItemId]}-radio-button`} + checked={isItemSelected(item[tableItemId])} + onChange={() => toggleItem(item[tableItemId])} + disabled={radioDisabledCheck !== undefined ? radioDisabledCheck(item) : undefined} /> )} @@ -299,11 +307,11 @@ export function CustomSelectionTable({ return ( {cells} @@ -331,7 +339,7 @@ export function CustomSelectionTable({ - + {renderSelectAll(true)} - + {renderHeaderCells()} {renderRows()} @@ -368,7 +376,7 @@ export function CustomSelectionTable({ handlePageChange(pageIndex)} @@ -379,13 +387,16 @@ export function CustomSelectionTable({ } CustomSelectionTable.propTypes = { + checkboxDisabledCheck: PropTypes.func, columns: PropTypes.array.isRequired, filterDefaultFields: PropTypes.array, filters: PropTypes.array, items: PropTypes.array.isRequired, + itemsPerPage: PropTypes.number, onTableChange: PropTypes.func.isRequired, + radioDisabledCheck: PropTypes.func, selectedId: PropTypes.array, singleSelection: PropTypes.bool, sortableProperties: PropTypes.object, - timeseriesOnly: PropTypes.bool, + tableItemId: PropTypes.string, }; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js b/x-pack/plugins/ml/public/application/components/custom_selection_table/index.js similarity index 100% rename from x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js rename to x-pack/plugins/ml/public/application/components/custom_selection_table/index.js diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js index 4eeef560dd6d1..7b104ea372ae5 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js @@ -6,7 +6,7 @@ import React, { Fragment, useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; -import { CustomSelectionTable } from '../custom_selection_table'; +import { CustomSelectionTable } from '../../custom_selection_table'; import { JobSelectorBadge } from '../job_selector_badge'; import { TimeRangeBar } from '../timerange_bar'; @@ -107,7 +107,7 @@ export function JobSelectorTable({ id: 'checkbox', isCheckbox: true, textOnly: false, - width: '24px', + width: '32px', }, { label: 'job ID', @@ -157,6 +157,9 @@ export function JobSelectorTable({ filterDefaultFields={!singleSelection ? JOB_FILTER_FIELDS : undefined} items={jobs} onTableChange={(selectionFromTable) => onSelection({ selectionFromTable })} + radioDisabledCheck={(item) => { + return timeseriesOnly && item.isSingleMetricViewerJob === false; + }} selectedIds={selectedIds} singleSelection={singleSelection} sortableProperties={sortableProperties} diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index 0f3b484402819..eed57aaf1b491 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -402,7 +402,14 @@ class RuleEditorFlyoutUI extends Component { } ) ); - this.closeFlyout(); + const updatedJob = mlJobService.getJob(anomaly.jobId); + const updatedDetector = updatedJob.analysis_config.detectors[detectorIndex]; + const updatedRules = updatedDetector.custom_rules; + if (!updatedRules) { + this.closeFlyout(); + } else { + this.setState({ job: { ...updatedJob } }); + } } else { toasts.addDanger( i18n.translate( diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js index 309e271ad26a4..c2f5820f84a9e 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js @@ -32,7 +32,7 @@ export function SelectRuleAction({ if (rules.length > 0) { ruleActionPanels = rules.map((rule, index) => { return ( - + { + if (arg === undefined) return false; const keys = Object.keys(arg); return keys.length === 1 && keys[0] === 'bool'; }; export const isQueryStringQuery = (arg: any): arg is QueryStringQuery => { + if (arg === undefined) return false; const keys = Object.keys(arg); return keys.length === 1 && keys[0] === 'query_string'; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 400902c152c9e..58343e26153cc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -17,6 +17,7 @@ export { IndexPattern, REFRESH_ANALYTICS_LIST_STATE, ANALYSIS_CONFIG_TYPE, + OUTLIER_ANALYSIS_METHOD, RegressionEvaluateResponse, getValuesFromResponse, loadEvalData, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx new file mode 100644 index 0000000000000..f957dcab2e87e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiForm } from '@elastic/eui'; + +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { AdvancedStepForm } from './advanced_step_form'; +import { AdvancedStepDetails } from './advanced_step_details'; +import { ANALYTICS_STEPS } from '../../page'; + +export const AdvancedStep: FC = ({ + actions, + state, + step, + setCurrentStep, + stepActivated, +}) => { + return ( + + {step === ANALYTICS_STEPS.ADVANCED && ( + + )} + {step !== ANALYTICS_STEPS.ADVANCED && stepActivated === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx new file mode 100644 index 0000000000000..a9c8b6d4040ad --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx @@ -0,0 +1,274 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { + UNSET_CONFIG_ITEM, + State, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; +import { ANALYTICS_STEPS } from '../../page'; + +function getStringValue(value: number | undefined) { + return value !== undefined ? `${value}` : UNSET_CONFIG_ITEM; +} + +export interface ListItems { + title: string; + description: string | JSX.Element; +} + +export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ + setCurrentStep, + state, +}) => { + const { form, isJobCreated } = state; + const { + computeFeatureInfluence, + dependentVariable, + eta, + featureBagFraction, + featureInfluenceThreshold, + gamma, + jobType, + lambda, + method, + maxTrees, + modelMemoryLimit, + nNeighbors, + numTopClasses, + numTopFeatureImportanceValues, + outlierFraction, + predictionFieldName, + randomizeSeed, + standardizationEnabled, + } = form; + + const isRegOrClassJob = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const advancedFirstCol: ListItems[] = []; + const advancedSecondCol: ListItems[] = []; + const advancedThirdCol: ListItems[] = []; + + const hyperFirstCol: ListItems[] = []; + const hyperSecondCol: ListItems[] = []; + const hyperThirdCol: ListItems[] = []; + + if (jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) { + advancedFirstCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.computeFeatureInfluence', + { + defaultMessage: 'Compute feature influence', + } + ), + description: computeFeatureInfluence, + }); + + advancedSecondCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.featureInfluenceThreshold', + { + defaultMessage: 'Feature influence threshold', + } + ), + description: getStringValue(featureInfluenceThreshold), + }); + + advancedThirdCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.modelMemoryLimit', { + defaultMessage: 'Model memory limit', + }), + description: `${modelMemoryLimit}`, + }); + + hyperFirstCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.nNeighbors', { + defaultMessage: 'N neighbors', + }), + description: getStringValue(nNeighbors), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.outlierFraction', { + defaultMessage: 'Outlier fraction', + }), + description: getStringValue(outlierFraction), + } + ); + + hyperSecondCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.method', { + defaultMessage: 'Method', + }), + description: method !== undefined ? method : UNSET_CONFIG_ITEM, + }); + + hyperThirdCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.standardizationEnabled', + { + defaultMessage: 'Standardization enabled', + } + ), + description: `${standardizationEnabled}`, + }); + } + + if (isRegOrClassJob) { + if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) { + advancedFirstCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.numTopClasses', { + defaultMessage: 'Top classes', + }), + description: `${numTopClasses}`, + }); + } + + advancedFirstCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.numTopFeatureImportanceValues', + { + defaultMessage: 'Top feature importance values', + } + ), + description: `${numTopFeatureImportanceValues}`, + }); + + hyperFirstCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.lambdaFields', { + defaultMessage: 'Lambda', + }), + description: getStringValue(lambda), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.eta', { + defaultMessage: 'Eta', + }), + description: getStringValue(eta), + } + ); + + advancedSecondCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.predictionFieldName', + { + defaultMessage: 'Prediction field name', + } + ), + description: predictionFieldName ? predictionFieldName : `${dependentVariable}_prediction`, + }); + + hyperSecondCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.maxTreesFields', { + defaultMessage: 'Max trees', + }), + description: getStringValue(maxTrees), + }, + { + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.featureBagFraction', + { + defaultMessage: 'Feature bag fraction', + } + ), + description: getStringValue(featureBagFraction), + } + ); + + advancedThirdCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.modelMemoryLimit', { + defaultMessage: 'Model memory limit', + }), + description: `${modelMemoryLimit}`, + }); + + hyperThirdCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.gamma', { + defaultMessage: 'Gamma', + }), + description: getStringValue(gamma), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.randomizedSeed', { + defaultMessage: 'Randomized seed', + }), + description: getStringValue(randomizeSeed), + } + ); + } + + return ( + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.advancedConfigDetailsTitle', { + defaultMessage: 'Advanced configuration', + })} +

+
+ + + + + + + + + + + + + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersDetailsTitle', { + defaultMessage: 'Hyper parameters', + })} +

+
+ + + + + + + + + + + + + + {!isJobCreated && ( + { + setCurrentStep(ANALYTICS_STEPS.ADVANCED); + }} + > + {i18n.translate('xpack.ml.dataframe.analytics.create.advancedDetails.editButtonText', { + defaultMessage: 'Edit', + })} + + )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx new file mode 100644 index 0000000000000..8b137ac72361c --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -0,0 +1,332 @@ +/* + * 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, { FC, Fragment, useMemo } from 'react'; +import { + EuiAccordion, + EuiFieldNumber, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { HyperParameters } from './hyper_parameters'; +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { getModelMemoryLimitErrors } from '../../../analytics_management/hooks/use_create_analytics_form/reducer'; +import { + ANALYSIS_CONFIG_TYPE, + NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN, +} from '../../../../common/analytics'; +import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYTICS_STEPS } from '../../page'; +import { ContinueButton } from '../continue_button'; +import { OutlierHyperParameters } from './outlier_hyper_parameters'; + +export function getNumberValue(value?: number) { + return value === undefined ? '' : +value; +} + +export const AdvancedStepForm: FC = ({ + actions, + state, + setCurrentStep, +}) => { + const { setFormState } = actions; + const { form, isJobCreated } = state; + const { + computeFeatureInfluence, + featureInfluenceThreshold, + jobType, + modelMemoryLimit, + modelMemoryLimitValidationResult, + numTopClasses, + numTopFeatureImportanceValues, + numTopFeatureImportanceValuesValid, + predictionFieldName, + } = form; + + const mmlErrors = useMemo(() => getModelMemoryLimitErrors(modelMemoryLimitValidationResult), [ + modelMemoryLimitValidationResult, + ]); + + const isRegOrClassJob = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const mmlInvalid = modelMemoryLimitValidationResult !== null; + + const outlierDetectionAdvancedConfig = ( + + + + { + setFormState({ + computeFeatureInfluence: e.target.value, + }); + }} + /> + + + + + + setFormState({ + featureInfluenceThreshold: e.target.value === '' ? undefined : +e.target.value, + }) + } + data-test-subj="mlAnalyticsCreateJobWizardFeatureInfluenceThresholdInput" + min={0} + max={1} + step={0.001} + value={getNumberValue(featureInfluenceThreshold)} + /> + + + + ); + + const regAndClassAdvancedConfig = ( + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesErrorText', + { + defaultMessage: 'Invalid maximum number of feature importance values.', + } + )} + , + ] + : []), + ]} + > + + setFormState({ + numTopFeatureImportanceValues: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={1} + value={getNumberValue(numTopFeatureImportanceValues)} + /> + +
+ + _prediction.', + } + )} + > + setFormState({ predictionFieldName: e.target.value })} + data-test-subj="mlAnalyticsCreateJobWizardPredictionFieldNameInput" + /> + + + + ); + + return ( + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.advancedConfigSectionTitle', { + defaultMessage: 'Advanced configuration', + })} +

+
+ + {jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && outlierDetectionAdvancedConfig} + {isRegOrClassJob && regAndClassAdvancedConfig} + {jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( + + + + setFormState({ + numTopClasses: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={1} + value={getNumberValue(numTopClasses)} + /> + + + )} + + + setFormState({ modelMemoryLimit: e.target.value })} + isInvalid={mmlInvalid} + data-test-subj="mlAnalyticsCreateJobWizardModelMemoryInput" + /> + + + + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersSectionTitle', { + defaultMessage: 'Hyper parameters', + })} +

+ + } + initialIsOpen={false} + data-test-subj="mlAnalyticsCreateJobWizardHyperParametersSection" + > + + {jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && ( + + )} + {isRegOrClassJob && } + +
+ + { + setCurrentStep(ANALYTICS_STEPS.DETAILS); + }} + /> +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx new file mode 100644 index 0000000000000..144a062106003 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx @@ -0,0 +1,188 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { EuiFieldNumber, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { getNumberValue } from './advanced_step_form'; + +const MAX_TREES_LIMIT = 2000; + +export const HyperParameters: FC = ({ actions, state }) => { + const { setFormState } = actions; + + const { eta, featureBagFraction, gamma, lambda, maxTrees, randomizeSeed } = state.form; + + return ( + + + + + setFormState({ lambda: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0} + value={getNumberValue(lambda)} + /> + + + + + + setFormState({ maxTrees: e.target.value === '' ? undefined : +e.target.value }) + } + isInvalid={maxTrees !== undefined && !Number.isInteger(maxTrees)} + step={1} + min={1} + max={MAX_TREES_LIMIT} + value={getNumberValue(maxTrees)} + /> + + + + + + setFormState({ gamma: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0} + value={getNumberValue(gamma)} + /> + + + + + + setFormState({ eta: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0.001} + max={1} + value={getNumberValue(eta)} + /> + + + + + + setFormState({ + featureBagFraction: e.target.value === '' ? undefined : +e.target.value, + }) + } + isInvalid={ + featureBagFraction !== undefined && + (featureBagFraction > 1 || featureBagFraction <= 0) + } + step={0.001} + max={1} + value={getNumberValue(featureBagFraction)} + /> + + + + + + setFormState({ randomizeSeed: e.target.value === '' ? undefined : +e.target.value }) + } + isInvalid={randomizeSeed !== undefined && typeof randomizeSeed !== 'number'} + value={getNumberValue(randomizeSeed)} + step={1} + /> + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/index.ts new file mode 100644 index 0000000000000..6a19e55b533c1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { AdvancedStep } from './advanced_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/outlier_hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/outlier_hyper_parameters.tsx new file mode 100644 index 0000000000000..dfe7969d8a6d9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/outlier_hyper_parameters.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { EuiFieldNumber, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { OUTLIER_ANALYSIS_METHOD } from '../../../../common/analytics'; +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { getNumberValue } from './advanced_step_form'; + +export const OutlierHyperParameters: FC = ({ actions, state }) => { + const { setFormState } = actions; + + const { method, nNeighbors, outlierFraction, standardizationEnabled } = state.form; + + return ( + + + + ({ + value: outlierMethod, + text: outlierMethod, + }))} + value={method} + hasNoInitialSelection={true} + onChange={(e) => { + setFormState({ method: e.target.value }); + }} + data-test-subj="mlAnalyticsCreateJobWizardMethodInput" + /> + + + + + + setFormState({ nNeighbors: e.target.value === '' ? undefined : +e.target.value }) + } + step={1} + min={1} + value={getNumberValue(nNeighbors)} + /> + + + + + + setFormState({ outlierFraction: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0} + max={1} + value={getNumberValue(outlierFraction)} + /> + + + + + { + setFormState({ standardizationEnabled: e.target.value }); + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx new file mode 100644 index 0000000000000..e437d27372a3e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -0,0 +1,35 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +function redirectToAnalyticsManagementPage() { + window.location.href = '#/data_frame_analytics?'; +} + +export const BackToListPanel: FC = () => ( + + + } + title={i18n.translate('xpack.ml.dataframe.analytics.create.analyticsListCardTitle', { + defaultMessage: 'Data Frame Analytics', + })} + description={i18n.translate( + 'xpack.ml.dataframe.analytics.create.analyticsListCardDescription', + { + defaultMessage: 'Return to the analytics management page.', + } + )} + onClick={redirectToAnalyticsManagementPage} + data-test-subj="analyticsWizardCardManagement" + /> + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/index.ts new file mode 100644 index 0000000000000..4da6561562879 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { BackToListPanel } from './back_to_list_panel'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx new file mode 100644 index 0000000000000..ad540285e49f0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -0,0 +1,207 @@ +/* + * 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, { FC, Fragment, memo, useEffect, useState } from 'react'; +import { EuiCallOut, EuiFormRow, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +// @ts-ignore no declaration +import { LEFT_ALIGNMENT, CENTER_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FieldSelectionItem } from '../../../../common/analytics'; +// @ts-ignore could not find declaration file +import { CustomSelectionTable } from '../../../../../components/custom_selection_table'; + +const columns = [ + { + id: 'checkbox', + isCheckbox: true, + textOnly: false, + width: '32px', + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.fieldNameColumn', { + defaultMessage: 'Field name', + }), + id: 'name', + isSortable: true, + alignment: LEFT_ALIGNMENT, + }, + { + id: 'mapping_types', + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.mappingColumn', { + defaultMessage: 'Mapping', + }), + isSortable: false, + alignment: LEFT_ALIGNMENT, + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isIncludedColumn', { + defaultMessage: 'Is included', + }), + id: 'is_included', + alignment: LEFT_ALIGNMENT, + isSortable: true, + // eslint-disable-next-line @typescript-eslint/camelcase + render: ({ is_included }: { is_included: boolean }) => (is_included ? 'Yes' : 'No'), + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isRequiredColumn', { + defaultMessage: 'Is required', + }), + id: 'is_required', + alignment: LEFT_ALIGNMENT, + isSortable: true, + // eslint-disable-next-line @typescript-eslint/camelcase + render: ({ is_required }: { is_required: boolean }) => (is_required ? 'Yes' : 'No'), + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.reasonColumn', { + defaultMessage: 'Reason', + }), + id: 'reason', + alignment: LEFT_ALIGNMENT, + isSortable: false, + }, +]; + +const checkboxDisabledCheck = (item: FieldSelectionItem) => + (item.is_included === false && !item.reason?.includes('in excludes list')) || + item.is_required === true; + +export const MemoizedAnalysisFieldsTable: FC<{ + excludes: string[]; + loadingItems: boolean; + setFormState: any; + tableItems: FieldSelectionItem[]; +}> = memo( + ({ excludes, loadingItems, setFormState, tableItems }) => { + const [sortableProperties, setSortableProperties] = useState(); + const [currentSelection, setCurrentSelection] = useState([]); + + useEffect(() => { + if (excludes.length > 0) { + setCurrentSelection(excludes); + } + }, []); + + // Only set form state on unmount to prevent re-renders due to props changing if exludes was updated on each selection + useEffect(() => { + return () => { + setFormState({ excludes: currentSelection }); + }; + }, [currentSelection]); + + useEffect(() => { + let sortablePropertyItems = []; + const defaultSortProperty = 'name'; + + sortablePropertyItems = [ + { + name: 'name', + getValue: (item: any) => item.name.toLowerCase(), + isAscending: true, + }, + { + name: 'is_included', + getValue: (item: any) => item.is_included, + isAscending: true, + }, + { + name: 'is_required', + getValue: (item: any) => item.is_required, + isAscending: true, + }, + ]; + const sortableProps = new SortableProperties(sortablePropertyItems, defaultSortProperty); + + setSortableProperties(sortableProps); + }, []); + + const filters = [ + { + type: 'field_value_selection', + field: 'is_included', + name: i18n.translate('xpack.ml.dataframe.analytics.create.excludedFilterLabel', { + defaultMessage: 'Is included', + }), + multiSelect: false, + options: [ + { + value: true, + view: ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.isIncludedOption', { + defaultMessage: 'Yes', + })} + + ), + }, + { + value: false, + view: ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.isNotIncludedOption', { + defaultMessage: 'No', + })} + + ), + }, + ], + }, + ]; + + return ( + + + + + {tableItems.length === 0 && ( + + + + )} + {tableItems.length > 0 && ( + + { + setCurrentSelection(selection); + }} + selectedIds={currentSelection} + singleSelection={false} + sortableProperties={sortableProperties} + tableItemId={'name'} + /> + + )} + + + ); + }, + (prevProps, nextProps) => prevProps.tableItems.length === nextProps.tableItems.length +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx new file mode 100644 index 0000000000000..220910535aafe --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx @@ -0,0 +1,35 @@ +/* + * 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, { FC } from 'react'; +import { EuiForm } from '@elastic/eui'; + +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { ConfigurationStepDetails } from './configuration_step_details'; +import { ConfigurationStepForm } from './configuration_step_form'; +import { ANALYTICS_STEPS } from '../../page'; + +export const ConfigurationStep: FC = ({ + actions, + state, + setCurrentStep, + step, + stepActivated, +}) => { + return ( + + {step === ANALYTICS_STEPS.CONFIGURATION && ( + + )} + {step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx new file mode 100644 index 0000000000000..6603af9aa302e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { + State, + UNSET_CONFIG_ITEM, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; +import { useMlContext } from '../../../../../contexts/ml'; +import { ANALYTICS_STEPS } from '../../page'; + +interface Props { + setCurrentStep: React.Dispatch>; + state: State; +} + +export const ConfigurationStepDetails: FC = ({ setCurrentStep, state }) => { + const mlContext = useMlContext(); + const { currentIndexPattern } = mlContext; + const { form, isJobCreated } = state; + const { dependentVariable, excludes, jobConfigQueryString, jobType, trainingPercent } = form; + + const isJobTypeWithDepVar = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const detailsFirstCol = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.sourceIndex', { + defaultMessage: 'Source index', + }), + description: currentIndexPattern.title || UNSET_CONFIG_ITEM, + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.Query', { + defaultMessage: 'Query', + }), + description: jobConfigQueryString || UNSET_CONFIG_ITEM, + }, + ]; + + const detailsSecondCol = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobType', { + defaultMessage: 'Job type', + }), + description: jobType! as string, + }, + ]; + + const detailsThirdCol = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.excludedFields', { + defaultMessage: 'Excluded fields', + }), + description: excludes.length > 0 ? excludes.join(', ') : UNSET_CONFIG_ITEM, + }, + ]; + + if (isJobTypeWithDepVar) { + detailsSecondCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.trainingPercent', { + defaultMessage: 'Training percent', + }), + description: `${trainingPercent}`, + }); + detailsThirdCol.unshift({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.dependentVariable', { + defaultMessage: 'Dependent variable', + }), + description: dependentVariable, + }); + } + + return ( + + + + + + + + + + + + + + {!isJobCreated && ( + { + setCurrentStep(ANALYTICS_STEPS.CONFIGURATION); + }} + > + {i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.editButtonText', { + defaultMessage: 'Edit', + })} + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx new file mode 100644 index 0000000000000..9446dfd4ed525 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -0,0 +1,449 @@ +/* + * 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, { FC, Fragment, useEffect, useRef } from 'react'; +import { EuiBadge, EuiComboBox, EuiFormRow, EuiRange, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; + +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { useMlContext } from '../../../../../contexts/ml'; + +import { + DfAnalyticsExplainResponse, + FieldSelectionItem, + ANALYSIS_CONFIG_TYPE, + TRAINING_PERCENT_MIN, + TRAINING_PERCENT_MAX, +} from '../../../../common/analytics'; +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { Messages } from '../../../analytics_management/components/create_analytics_form/messages'; +import { + DEFAULT_MODEL_MEMORY_LIMIT, + getJobConfigFromFormState, + State, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { shouldAddAsDepVarOption } from '../../../analytics_management/components/create_analytics_form/form_options_validation'; +import { ml } from '../../../../../services/ml_api_service'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; + +import { ANALYTICS_STEPS } from '../../page'; +import { ContinueButton } from '../continue_button'; +import { JobType } from './job_type'; +import { SupportedFieldsMessage } from './supported_fields_message'; +import { MemoizedAnalysisFieldsTable } from './analysis_fields_table'; +import { DataGrid } from '../../../../../components/data_grid'; +import { useIndexData } from '../../hooks'; +import { ExplorationQueryBar } from '../../../analytics_exploration/components/exploration_query_bar'; +import { useSavedSearch } from './use_saved_search'; + +const requiredFieldsErrorText = i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', + { + defaultMessage: 'At least one field must be included in the analysis.', + } +); + +export const ConfigurationStepForm: FC = ({ + actions, + state, + setCurrentStep, +}) => { + const mlContext = useMlContext(); + const { currentSavedSearch, currentIndexPattern } = mlContext; + const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch(); + + const { initiateWizard, setEstimatedModelMemoryLimit, setFormState } = actions; + const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; + const firstUpdate = useRef(true); + const { + dependentVariable, + dependentVariableFetchFail, + dependentVariableOptions, + excludes, + excludesTableItems, + fieldOptionsFetchFail, + jobConfigQuery, + jobConfigQueryString, + jobType, + loadingDepVarOptions, + loadingFieldOptions, + maxDistinctValuesError, + modelMemoryLimit, + previousJobType, + requiredFieldsError, + trainingPercent, + } = form; + + const setJobConfigQuery = ({ query, queryString }: { query: any; queryString: string }) => { + setFormState({ jobConfigQuery: query, jobConfigQueryString: queryString }); + }; + + const indexData = useIndexData( + currentIndexPattern, + savedSearchQuery !== undefined ? savedSearchQuery : jobConfigQuery + ); + + const indexPreviewProps = { + ...indexData, + dataTestSubj: 'mlAnalyticsCreationDataGrid', + toastNotifications: getToastNotifications(), + }; + + const isJobTypeWithDepVar = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const dependentVariableEmpty = isJobTypeWithDepVar && dependentVariable === ''; + + const isStepInvalid = + dependentVariableEmpty || + jobType === undefined || + maxDistinctValuesError !== undefined || + requiredFieldsError !== undefined; + + const loadDepVarOptions = async (formState: State['form']) => { + setFormState({ + loadingDepVarOptions: true, + maxDistinctValuesError: undefined, + }); + try { + if (currentIndexPattern !== undefined) { + const formStateUpdate: { + loadingDepVarOptions: boolean; + dependentVariableFetchFail: boolean; + dependentVariableOptions: State['form']['dependentVariableOptions']; + dependentVariable?: State['form']['dependentVariable']; + } = { + loadingDepVarOptions: false, + dependentVariableFetchFail: false, + dependentVariableOptions: [] as State['form']['dependentVariableOptions'], + }; + + // Get fields and filter for supported types for job type + const { fields } = newJobCapsService; + + let resetDependentVariable = true; + for (const field of fields) { + if (shouldAddAsDepVarOption(field, jobType)) { + formStateUpdate.dependentVariableOptions.push({ + label: field.id, + }); + + if (formState.dependentVariable === field.id) { + resetDependentVariable = false; + } + } + } + + if (resetDependentVariable) { + formStateUpdate.dependentVariable = ''; + } + + setFormState(formStateUpdate); + } + } catch (e) { + setFormState({ loadingDepVarOptions: false, dependentVariableFetchFail: true }); + } + }; + + const debouncedGetExplainData = debounce(async () => { + const jobTypeChanged = previousJobType !== jobType; + const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; + const shouldUpdateEstimatedMml = + !firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === ''; + + if (firstUpdate.current) { + firstUpdate.current = false; + } + // Reset if jobType changes (jobType requires dependent_variable to be set - + // which won't be the case if switching from outlier detection) + if (jobTypeChanged) { + setFormState({ + loadingFieldOptions: true, + }); + } + + try { + const jobConfig = getJobConfigFromFormState(form); + delete jobConfig.dest; + delete jobConfig.model_memory_limit; + const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( + jobConfig + ); + const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk; + + if (shouldUpdateEstimatedMml) { + setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); + } + + const fieldSelection: FieldSelectionItem[] | undefined = resp.field_selection; + + let hasRequiredFields = false; + if (fieldSelection) { + for (let i = 0; i < fieldSelection.length; i++) { + const field = fieldSelection[i]; + if (field.is_included === true && field.is_required === false) { + hasRequiredFields = true; + break; + } + } + } + + // If job type has changed load analysis field options again + if (jobTypeChanged) { + setFormState({ + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + excludesTableItems: fieldSelection ? fieldSelection : [], + loadingFieldOptions: false, + fieldOptionsFetchFail: false, + maxDistinctValuesError: undefined, + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + }); + } else { + setFormState({ + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + }); + } + } catch (e) { + let errorMessage; + if ( + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + e.body && + e.body.message !== undefined && + e.body.message.includes('status_exception') && + (e.body.message.includes('must have at most') || + e.body.message.includes('must have at least')) + ) { + errorMessage = e.body.message; + } + const fallbackModelMemoryLimit = + jobType !== undefined + ? DEFAULT_MODEL_MEMORY_LIMIT[jobType] + : DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection; + setEstimatedModelMemoryLimit(fallbackModelMemoryLimit); + setFormState({ + fieldOptionsFetchFail: true, + maxDistinctValuesError: errorMessage, + loadingFieldOptions: false, + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: fallbackModelMemoryLimit } : {}), + }); + } + }, 300); + + useEffect(() => { + initiateWizard(); + }, []); + + useEffect(() => { + setFormState({ sourceIndex: currentIndexPattern.title }); + }, []); + + useEffect(() => { + if (savedSearchQueryStr !== undefined) { + setFormState({ jobConfigQuery: savedSearchQuery, jobConfigQueryString: savedSearchQueryStr }); + } + }, [JSON.stringify(savedSearchQuery), savedSearchQueryStr]); + + useEffect(() => { + if (isJobTypeWithDepVar) { + loadDepVarOptions(form); + } + }, [jobType]); + + useEffect(() => { + const hasBasicRequiredFields = jobType !== undefined; + + const hasRequiredAnalysisFields = + (isJobTypeWithDepVar && dependentVariable !== '') || + jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; + + if (hasBasicRequiredFields && hasRequiredAnalysisFields) { + debouncedGetExplainData(); + } + + return () => { + debouncedGetExplainData.cancel(); + }; + }, [jobType, dependentVariable, trainingPercent, JSON.stringify(excludes), jobConfigQueryString]); + + return ( + + + + + {savedSearchQuery === undefined && ( + + + + )} + + {savedSearchQuery !== undefined && ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.savedSearchLabel', { + defaultMessage: 'Saved search', + })} + + )} + + {savedSearchQuery !== undefined + ? currentSavedSearch?.attributes.title + : currentIndexPattern.title} + + + } + fullWidth + > + + + {isJobTypeWithDepVar && ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.dependentVariableOptionsFetchError', + { + defaultMessage: + 'There was a problem fetching fields. Please refresh the page and try again.', + } + )} + , + ] + : []), + ...(fieldOptionsFetchFail === true && maxDistinctValuesError !== undefined + ? [ + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.dependentVariableMaxDistictValuesError', + { + defaultMessage: 'Invalid. {message}', + values: { message: maxDistinctValuesError }, + } + )} + , + ] + : []), + ]} + > + + setFormState({ + dependentVariable: selectedOptions[0].label || '', + }) + } + isClearable={false} + isInvalid={dependentVariable === ''} + data-test-subj="mlAnalyticsCreateJobWizardDependentVariableSelect" + /> + + + )} + + + + + {isJobTypeWithDepVar && ( + + setFormState({ trainingPercent: +e.target.value })} + data-test-subj="mlAnalyticsCreateJobWizardTrainingPercentSlider" + /> + + )} + + { + setCurrentStep(ANALYTICS_STEPS.ADVANCED); + }} + /> + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/index.ts new file mode 100644 index 0000000000000..ba67a6f080d45 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ConfigurationStep } from './configuration_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx new file mode 100644 index 0000000000000..f31c9cd28f65a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -0,0 +1,83 @@ +/* + * 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, { Fragment, FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common'; + +import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; + +interface Props { + type: AnalyticsJobType; + setFormState: React.Dispatch>; +} + +export const JobType: FC = ({ type, setFormState }) => { + const outlierHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText', + { + defaultMessage: + 'Outlier detection jobs require a source index that is mapped as a table-like data structure and analyze only numeric and boolean fields. Use the advanced editor to add custom options to the configuration.', + } + ); + + const regressionHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText', + { + defaultMessage: + 'Regression jobs analyze only numeric fields. Use the advanced editor to apply custom options, such as the prediction field name.', + } + ); + + const classificationHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.classificationHelpText', + { + defaultMessage: + 'Classification jobs require a source index that is mapped as a table-like data structure and support fields that are numeric, boolean, text, keyword, or ip. Use the advanced editor to apply custom options, such as the prediction field name.', + } + ); + + const helpText = { + [ANALYSIS_CONFIG_TYPE.REGRESSION]: regressionHelpText, + [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: outlierHelpText, + [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: classificationHelpText, + }; + + return ( + + + ({ + value: jobType, + text: jobType.replace(/_/g, ' '), + 'data-test-subj': `mlAnalyticsCreation-${jobType}-option`, + }))} + value={type} + hasNoInitialSelection={true} + onChange={(e) => { + const value = e.target.value as AnalyticsJobType; + setFormState({ + previousJobType: type, + jobType: value, + excludes: [], + requiredFieldsError: undefined, + }); + }} + data-test-subj="mlAnalyticsCreateJobWizardJobTypeSelect" + /> + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx new file mode 100644 index 0000000000000..fe13cc1d6edfc --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -0,0 +1,120 @@ +/* + * 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, { FC, Fragment, useState, useEffect } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; +import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; +import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields'; +import { + OMIT_FIELDS, + CATEGORICAL_TYPES, +} from '../../../analytics_management/components/create_analytics_form/form_options_validation'; +import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; + +const containsClassificationFieldsCb = ({ name, type }: Field) => + !OMIT_FIELDS.includes(name) && + name !== EVENT_RATE_FIELD_ID && + (BASIC_NUMERICAL_TYPES.has(type) || + CATEGORICAL_TYPES.has(type) || + type === ES_FIELD_TYPES.BOOLEAN); + +const containsRegressionFieldsCb = ({ name, type }: Field) => + !OMIT_FIELDS.includes(name) && + name !== EVENT_RATE_FIELD_ID && + (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + +const containsOutlierFieldsCb = ({ name, type }: Field) => + !OMIT_FIELDS.includes(name) && name !== EVENT_RATE_FIELD_ID && BASIC_NUMERICAL_TYPES.has(type); + +const callbacks: Record boolean> = { + [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: containsClassificationFieldsCb, + [ANALYSIS_CONFIG_TYPE.REGRESSION]: containsRegressionFieldsCb, + [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: containsOutlierFieldsCb, +}; + +const messages: Record = { + [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: ( + + ), + [ANALYSIS_CONFIG_TYPE.REGRESSION]: ( + + ), + [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: ( + + ), +}; + +interface Props { + jobType: AnalyticsJobType; +} + +export const SupportedFieldsMessage: FC = ({ jobType }) => { + const [sourceIndexContainsSupportedFields, setSourceIndexContainsSupportedFields] = useState< + boolean + >(true); + const [sourceIndexFieldsCheckFailed, setSourceIndexFieldsCheckFailed] = useState(false); + const { fields } = newJobCapsService; + + // Find out if index pattern contains supported fields for job type. Provides a hint in the form + // that job may not run correctly if no supported fields are found. + const validateFields = () => { + if (fields && jobType !== undefined) { + try { + const containsSupportedFields: boolean = fields.some(callbacks[jobType]); + + setSourceIndexContainsSupportedFields(containsSupportedFields); + setSourceIndexFieldsCheckFailed(false); + } catch (e) { + setSourceIndexFieldsCheckFailed(true); + } + } + }; + + useEffect(() => { + if (jobType !== undefined) { + setSourceIndexContainsSupportedFields(true); + setSourceIndexFieldsCheckFailed(false); + validateFields(); + } + }, [jobType]); + + if (sourceIndexContainsSupportedFields === true) return null; + + if (sourceIndexFieldsCheckFailed === true) { + return ( + + + + + ); + } + + return ( + + + {jobType !== undefined && messages[jobType]} + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts new file mode 100644 index 0000000000000..856358538b26f --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts @@ -0,0 +1,50 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useMlContext } from '../../../../../contexts/ml'; +import { esQuery, esKuery } from '../../../../../../../../../../src/plugins/data/public'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search'; +import { getQueryFromSavedSearch } from '../../../../../util/index_utils'; + +export function useSavedSearch() { + const [savedSearchQuery, setSavedSearchQuery] = useState(undefined); + const [savedSearchQueryStr, setSavedSearchQueryStr] = useState(undefined); + + const mlContext = useMlContext(); + const { currentSavedSearch, currentIndexPattern, kibanaConfig } = mlContext; + + const getQueryData = () => { + let qry; + let qryString; + + if (currentSavedSearch !== null) { + const { query } = getQueryFromSavedSearch(currentSavedSearch); + const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; + qryString = query.query; + + if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { + const ast = esKuery.fromKueryExpression(qryString); + qry = esKuery.toElasticsearchQuery(ast, currentIndexPattern); + } else { + qry = esQuery.luceneStringToDsl(qryString); + esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); + } + + setSavedSearchQuery(qry); + setSavedSearchQueryStr(qryString); + } + }; + + useEffect(() => { + getQueryData(); + }, []); + + return { + savedSearchQuery, + savedSearchQueryStr, + }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/continue_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/continue_button.tsx new file mode 100644 index 0000000000000..6e95a0a246573 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/continue_button.tsx @@ -0,0 +1,34 @@ +/* + * 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, { FC } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const continueButtonText = i18n.translate( + 'xpack.ml.dataframe.analytics.creation.continueButtonText', + { + defaultMessage: 'Continue', + } +); + +export const ContinueButton: FC<{ isDisabled: boolean; onClick: any }> = ({ + isDisabled, + onClick, +}) => ( + + + + {continueButtonText} + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx new file mode 100644 index 0000000000000..2dda5f5d819b7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useState } from 'react'; +import { + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { Messages } from '../../../analytics_management/components/create_analytics_form/messages'; +import { ANALYTICS_STEPS } from '../../page'; +import { BackToListPanel } from '../back_to_list_panel'; + +interface Props extends CreateAnalyticsFormProps { + step: ANALYTICS_STEPS; +} + +export const CreateStep: FC = ({ actions, state, step }) => { + const { createAnalyticsJob, startAnalyticsJob } = actions; + const { + isAdvancedEditorValidJson, + isJobCreated, + isJobStarted, + isModalButtonDisabled, + isValid, + requestMessages, + } = state; + + const [checked, setChecked] = useState(true); + + if (step !== ANALYTICS_STEPS.CREATE) return null; + + const handleCreation = async () => { + await createAnalyticsJob(); + + if (checked) { + startAnalyticsJob(); + } + }; + + return ( + + {!isJobCreated && !isJobStarted && ( + + + + setChecked(e.target.checked)} + /> + + + + + {i18n.translate('xpack.ml.dataframe.analytics.create.wizardCreateButton', { + defaultMessage: 'Create', + })} + + + + )} + + + {isJobCreated === true && } + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/index.ts new file mode 100644 index 0000000000000..01c8e4bff934d --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { CreateStep } from './create_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx new file mode 100644 index 0000000000000..a40813ed2fc3e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiForm } from '@elastic/eui'; + +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { DetailsStepDetails } from './details_step_details'; +import { DetailsStepForm } from './details_step_form'; +import { ANALYTICS_STEPS } from '../../page'; + +export const DetailsStep: FC = ({ + actions, + state, + setCurrentStep, + step, + stepActivated, +}) => { + return ( + + {step === ANALYTICS_STEPS.DETAILS && ( + + )} + {step !== ANALYTICS_STEPS.DETAILS && stepActivated === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx new file mode 100644 index 0000000000000..a4d86b48006e8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx @@ -0,0 +1,87 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { State } from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYTICS_STEPS } from '../../page'; + +export interface ListItems { + title: string; + description: string | JSX.Element; +} + +export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ + setCurrentStep, + state, +}) => { + const { form, isJobCreated } = state; + const { description, jobId, destinationIndex } = form; + + const detailsFirstCol: ListItems[] = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobId', { + defaultMessage: 'Job ID', + }), + description: jobId, + }, + ]; + + const detailsSecondCol: ListItems[] = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', { + defaultMessage: 'Job description', + }), + description, + }, + ]; + + const detailsThirdCol: ListItems[] = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.destIndex', { + defaultMessage: 'Destination index', + }), + description: destinationIndex || '', + }, + ]; + + return ( + + + + + + + + + + + + + + {!isJobCreated && ( + { + setCurrentStep(ANALYTICS_STEPS.DETAILS); + }} + > + {i18n.translate('xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText', { + defaultMessage: 'Edit', + })} + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx new file mode 100644 index 0000000000000..67f8472e7ad14 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -0,0 +1,221 @@ +/* + * 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, { FC, Fragment, useRef } from 'react'; +import { EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiSwitch, EuiTextArea } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useMlKibana } from '../../../../../contexts/kibana'; +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; +import { ContinueButton } from '../continue_button'; +import { ANALYTICS_STEPS } from '../../page'; + +export const DetailsStepForm: FC = ({ + actions, + state, + setCurrentStep, +}) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + + const { setFormState } = actions; + const { form, isJobCreated } = state; + const { + createIndexPattern, + description, + destinationIndex, + destinationIndexNameEmpty, + destinationIndexNameExists, + destinationIndexNameValid, + destinationIndexPatternTitleExists, + jobId, + jobIdEmpty, + jobIdExists, + jobIdInvalidMaxLength, + jobIdValid, + } = form; + const forceInput = useRef(null); + + const isStepInvalid = + jobIdEmpty === true || + jobIdExists === true || + jobIdValid === false || + destinationIndexNameEmpty === true || + destinationIndexNameValid === false || + (destinationIndexPatternTitleExists === true && createIndexPattern === true); + + return ( + + + { + if (input) { + forceInput.current = input; + } + }} + disabled={isJobCreated} + placeholder={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdPlaceholder', { + defaultMessage: 'Job ID', + })} + value={jobId} + onChange={(e) => setFormState({ jobId: e.target.value })} + aria-label={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdInputAriaLabel', { + defaultMessage: 'Choose a unique analytics job ID.', + })} + isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists || jobIdEmpty} + data-test-subj="mlAnalyticsCreateJobFlyoutJobIdInput" + /> + + + { + const value = e.target.value; + setFormState({ description: value }); + }} + data-test-subj="mlDFAnalyticsJobCreationJobDescription" + /> + + + {i18n.translate('xpack.ml.dataframe.analytics.create.destinationIndexInvalidError', { + defaultMessage: 'Invalid destination index name.', + })} +
+ + {i18n.translate( + 'xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink', + { + defaultMessage: 'Learn more about index name limitations.', + } + )} + +
, + ] + } + > + setFormState({ destinationIndex: e.target.value })} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.create.destinationIndexInputAriaLabel', + { + defaultMessage: 'Choose a unique destination index name.', + } + )} + isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid} + data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput" + /> + + + setFormState({ createIndexPattern: !createIndexPattern })} + data-test-subj="mlAnalyticsCreateJobWizardCreateIndexPatternSwitch" + /> + + + { + setCurrentStep(ANALYTICS_STEPS.CREATE); + }} + /> + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/index.ts new file mode 100644 index 0000000000000..6cadd87d97e27 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { DetailsStep } from './details_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/index.ts new file mode 100644 index 0000000000000..efd7c0634bed4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ConfigurationStep } from './configuration_step/index'; +export { AdvancedStep } from './advanced_step/index'; +export { DetailsStep } from './details_step/index'; +export { CreateStep } from './create_step/index'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/index.ts new file mode 100644 index 0000000000000..5199fa1b6e4c6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { useIndexData } from './use_index_data'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts new file mode 100644 index 0000000000000..e8f25584201e3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -0,0 +1,103 @@ +/* + * 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 { useEffect } from 'react'; + +import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + useDataGrid, + useRenderCellValue, + EsSorting, + SearchResponse7, + UseIndexDataReturnType, +} from '../../../../components/data_grid'; +import { getErrorMessage } from '../../../../../../common/util/errors'; +import { INDEX_STATUS } from '../../../common/analytics'; +import { ml } from '../../../../services/ml_api_service'; + +type IndexSearchResponse = SearchResponse7; + +export const useIndexData = (indexPattern: IndexPattern, query: any): UseIndexDataReturnType => { + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + + // EuiDataGrid State + const columns = [ + ...indexPatternFields.map((id) => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { id, schema }; + }), + ]; + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + const getIndexData = async function () { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const esSearchRequest = { + index: indexPattern.title, + body: { + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + query, // isDefaultQuery(query) ? matchAllQuery : query, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await ml.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map((d) => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/index.ts new file mode 100644 index 0000000000000..7e2d651439ae3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { Page } from './page'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx new file mode 100644 index 0000000000000..def862b859162 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -0,0 +1,191 @@ +/* + * 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, { FC, useEffect, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiSteps, + EuiStepStatus, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useMlContext } from '../../../contexts/ml'; +import { newJobCapsService } from '../../../services/new_job_capabilities_service'; +import { useCreateAnalyticsForm } from '../analytics_management/hooks/use_create_analytics_form'; +import { CreateAnalyticsAdvancedEditor } from '../analytics_management/components/create_analytics_advanced_editor'; +import { AdvancedStep, ConfigurationStep, CreateStep, DetailsStep } from './components'; + +export enum ANALYTICS_STEPS { + CONFIGURATION, + ADVANCED, + DETAILS, + CREATE, +} + +export const Page: FC = () => { + const [currentStep, setCurrentStep] = useState(ANALYTICS_STEPS.CONFIGURATION); + const [activatedSteps, setActivatedSteps] = useState([true, false, false, false]); + + const mlContext = useMlContext(); + const { currentIndexPattern } = mlContext; + + const createAnalyticsForm = useCreateAnalyticsForm(); + const { isAdvancedEditorEnabled } = createAnalyticsForm.state; + const { jobType } = createAnalyticsForm.state.form; + const { switchToAdvancedEditor } = createAnalyticsForm.actions; + + useEffect(() => { + if (activatedSteps[currentStep] === false) { + activatedSteps.splice(currentStep, 1, true); + setActivatedSteps(activatedSteps); + } + }, [currentStep]); + + useEffect(() => { + if (currentIndexPattern) { + (async function () { + await newJobCapsService.initializeFromIndexPattern(currentIndexPattern, false, false); + })(); + } + }, []); + + const analyticsWizardSteps = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.configurationStepTitle', { + defaultMessage: 'Configuration', + }), + children: ( + + ), + status: + currentStep >= ANALYTICS_STEPS.CONFIGURATION ? undefined : ('incomplete' as EuiStepStatus), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.advancedStepTitle', { + defaultMessage: 'Additional options', + }), + children: ( + + ), + status: currentStep >= ANALYTICS_STEPS.ADVANCED ? undefined : ('incomplete' as EuiStepStatus), + 'data-test-subj': 'mlAnalyticsCreateJobWizardAdvancedStep', + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.detailsStepTitle', { + defaultMessage: 'Job details', + }), + children: ( + + ), + status: currentStep >= ANALYTICS_STEPS.DETAILS ? undefined : ('incomplete' as EuiStepStatus), + 'data-test-subj': 'mlAnalyticsCreateJobWizardDetailsStep', + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.createStepTitle', { + defaultMessage: 'Create', + }), + children: , + status: currentStep >= ANALYTICS_STEPS.CREATE ? undefined : ('incomplete' as EuiStepStatus), + 'data-test-subj': 'mlAnalyticsCreateJobWizardCreateStep', + }, + ]; + + return ( + + + + + + + + +

+ +

+
+
+ +

+ +

+
+
+
+ {isAdvancedEditorEnabled === false && ( + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch', + { + defaultMessage: 'Switch to json editor', + } + )} + + + + + )} +
+ + {isAdvancedEditorEnabled === true && ( + + )} + {isAdvancedEditorEnabled === false && ( + + )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index f95e6a93058ba..8c158c1ca14a0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Dispatch, FC, SetStateAction, useState } from 'react'; +import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'; import { EuiCode, EuiInputPopover } from '@elastic/eui'; @@ -30,11 +30,15 @@ interface ErrorMessage { interface ExplorationQueryBarProps { indexPattern: IIndexPattern; setSearchQuery: Dispatch>; + includeQueryString?: boolean; + defaultQueryString?: string; } export const ExplorationQueryBar: FC = ({ indexPattern, setSearchQuery, + includeQueryString = false, + defaultQueryString, }) => { // The internal state of the input query bar updated on every key stroke. const [searchInput, setSearchInput] = useState({ @@ -44,20 +48,34 @@ export const ExplorationQueryBar: FC = ({ const [errorMessage, setErrorMessage] = useState(undefined); + useEffect(() => { + if (defaultQueryString !== undefined) { + setSearchInput({ query: defaultQueryString, language: SEARCH_QUERY_LANGUAGE.KUERY }); + } + }, []); + const searchChangeHandler = (query: Query) => setSearchInput(query); const searchSubmitHandler = (query: Query) => { try { switch (query.language) { case SEARCH_QUERY_LANGUAGE.KUERY: + const convertedKQuery = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ); setSearchQuery( - esKuery.toElasticsearchQuery( - esKuery.fromKueryExpression(query.query as string), - indexPattern - ) + includeQueryString + ? { queryString: query.query, query: convertedKQuery } + : convertedKQuery ); return; case SEARCH_QUERY_LANGUAGE.LUCENE: - setSearchQuery(esQuery.luceneStringToDsl(query.query as string)); + const convertedLQuery = esQuery.luceneStringToDsl(query.query as string); + setSearchQuery( + includeQueryString + ? { queryString: query.query, query: convertedLQuery } + : convertedLQuery + ); return; } } catch (e) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 0154f92576c4a..58f8528236bb9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -73,6 +73,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = ); } + /* eslint-disable-next-line react-hooks/rules-of-hooks */ const colorRange = useColorRange( COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 72514c91ff58b..295a3988e1b58 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; -import { DeepReadonly } from '../../../../../../../common/types/common'; +// import { DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission, @@ -21,7 +21,7 @@ import { isClassificationAnalysis, } from '../../../../common/analytics'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { CloneAction } from './action_clone'; +// import { CloneAction } from './action_clone'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; import { stopAnalytics } from '../../services/analytics_service'; @@ -106,10 +106,10 @@ export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => { return ; }, }, - { - render: (item: DeepReadonly) => { - return ; - }, - }, + // { + // render: (item: DeepReadonly) => { + // return ; + // }, + // }, ]; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 4a99c042b108b..bb012a2190859 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -53,6 +53,7 @@ import { CreateAnalyticsButton } from '../create_analytics_button'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; import { CreateAnalyticsFlyoutWrapper } from '../create_analytics_flyout_wrapper'; import { getSelectedJobIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; +import { SourceSelection } from '../source_selection'; function getItemIdToExpandedRowMap( itemIds: DataFrameAnalyticsId[], @@ -90,6 +91,7 @@ export const DataFrameAnalyticsList: FC = ({ createAnalyticsForm, }) => { const [isInitialized, setIsInitialized] = useState(false); + const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); const [filterActive, setFilterActive] = useState(false); @@ -271,7 +273,7 @@ export const DataFrameAnalyticsList: FC = ({ !isManagementTable && createAnalyticsForm ? [ setIsSourceIndexModalVisible(true)} isDisabled={disabled} data-test-subj="mlAnalyticsCreateFirstButton" > @@ -287,6 +289,9 @@ export const DataFrameAnalyticsList: FC = ({ {!isManagementTable && createAnalyticsForm && ( )} + {isSourceIndexModalVisible === true && ( + setIsSourceIndexModalVisible(false)} /> + )} ); } @@ -402,7 +407,10 @@ export const DataFrameAnalyticsList: FC = ({ {!isManagementTable && createAnalyticsForm && ( - + )}
@@ -435,6 +443,9 @@ export const DataFrameAnalyticsList: FC = ({ {!isManagementTable && createAnalyticsForm?.state.isModalVisible && ( )} + {isSourceIndexModalVisible === true && ( + setIsSourceIndexModalVisible(false)} /> + )} ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index 48eb95948bb12..17b905cab135b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -23,11 +23,14 @@ import { XJsonMode } from '../../../../../../../shared_imports'; const xJsonMode = new XJsonMode(); import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { CreateStep } from '../../../analytics_creation/components/create_step'; +import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; -export const CreateAnalyticsAdvancedEditor: FC = ({ actions, state }) => { +export const CreateAnalyticsAdvancedEditor: FC = (props) => { + const { actions, state } = props; const { setAdvancedEditorRawString, setFormState } = actions; - const { advancedEditorMessages, advancedEditorRawString, isJobCreated, requestMessages } = state; + const { advancedEditorMessages, advancedEditorRawString, isJobCreated } = state; const { createIndexPattern, @@ -56,120 +59,105 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac return ( - {requestMessages.map((requestMessage, i) => ( + + { + if (input) { + forceInput.current = input; + } + }} + disabled={isJobCreated} + placeholder="analytics job ID" + value={jobId} + onChange={(e) => setFormState({ jobId: e.target.value })} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInputAriaLabel', + { + defaultMessage: 'Choose a unique analytics job ID.', + } + )} + isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists} + /> + + + + + + + {advancedEditorMessages.map((advancedEditorMessage, i) => ( - {requestMessage.error !== undefined ?

{requestMessage.error}

: null} + {advancedEditorMessage.message !== '' && advancedEditorMessage.error !== undefined ? ( +

{advancedEditorMessage.error}

+ ) : null}
- +
))} {!isJobCreated && ( - - { - if (input) { - forceInput.current = input; - } - }} - disabled={isJobCreated} - placeholder="analytics job ID" - value={jobId} - onChange={(e) => setFormState({ jobId: e.target.value })} - aria-label={i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInputAriaLabel', - { - defaultMessage: 'Choose a unique analytics job ID.', - } - )} - isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists} - /> - - - - - - - {advancedEditorMessages.map((advancedEditorMessage, i) => ( - - - {advancedEditorMessage.message !== '' && - advancedEditorMessage.error !== undefined ? ( -

{advancedEditorMessage.error}

- ) : null} -
- -
- ))} = ({ ac
)} + +
); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/_index.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/_index.scss new file mode 100644 index 0000000000000..14ff9de7ded4d --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/_index.scss @@ -0,0 +1,4 @@ +.dataFrameAnalyticsCreateSearchDialog { + width: $euiSizeL * 30; + min-height: $euiSizeL * 25; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx index 10565fb4d7a93..8e9b09ef41fa8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -35,7 +35,9 @@ describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const { getLastHookValue } = getMountedHook(); const props = getLastHookValue(); - const wrapper = mount(); + const wrapper = mount( + + ); expect(wrapper.find('EuiButton').text()).toBe('Create analytics job'); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index 952bd48468b0a..595a1cf73e189 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -10,15 +10,26 @@ import { i18n } from '@kbn/i18n'; import { createPermissionFailureMessage } from '../../../../../capabilities/check_capabilities'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -export const CreateAnalyticsButton: FC = (props) => { - const { disabled } = props.state; - const { openModal } = props.actions; +interface Props extends CreateAnalyticsFormProps { + setIsSourceIndexModalVisible: React.Dispatch>; +} + +export const CreateAnalyticsButton: FC = ({ + state, + actions, + setIsSourceIndexModalVisible, +}) => { + const { disabled } = state; + + const handleClick = () => { + setIsSourceIndexModalVisible(true); + }; const button = ( = ({ messages }) => { {messages.map((requestMessage, i) => ( void; +} + +export const SourceSelection: FC = ({ onClose }) => { + const { uiSettings, savedObjects } = useMlKibana().services; + + const onSearchSelected = (id: string, type: string) => { + window.location.href = `ml#/data_frame_analytics/new_job?${ + type === 'index-pattern' ? 'index' : 'savedSearchId' + }=${encodeURIComponent(id)}`; + }; + + return ( + <> + + + + + {' '} + /{' '} + + + + + 'search', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search', + { + defaultMessage: 'Saved search', + } + ), + }, + { + type: 'index-pattern', + getIconForSavedObject: () => 'indexPatternApp', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern', + { + defaultMessage: 'Index pattern', + } + ), + }, + ]} + fixedPageSize={fixedPageSize} + uiSettings={uiSettings} + savedObjects={savedObjects} + /> + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 66e4103f5bb41..c42e03b584a56 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -72,6 +72,7 @@ export interface ActionDispatchers { closeModal: () => void; createAnalyticsJob: () => void; openModal: () => Promise; + initiateWizard: () => Promise; resetAdvancedEditorMessages: () => void; setAdvancedEditorRawString: (payload: State['advancedEditorRawString']) => void; setFormState: (payload: Partial) => void; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts index e5e56c084f7b9..8ec415bc2abb4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts @@ -5,4 +5,9 @@ */ export { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from './state'; -export { useCreateAnalyticsForm, CreateAnalyticsFormProps } from './use_create_analytics_form'; +export { + AnalyticsCreationStep, + useCreateAnalyticsForm, + CreateAnalyticsFormProps, + CreateAnalyticsStepProps, +} from './use_create_analytics_form'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 4ff7deab34f26..387ce89ee4120 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -11,6 +11,7 @@ import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { + FieldSelectionItem, isClassificationAnalysis, isRegressionAnalysis, DataFrameAnalyticsId, @@ -26,7 +27,8 @@ export enum DEFAULT_MODEL_MEMORY_LIMIT { classification = '100mb', } -export const DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES = 2; +export const DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES = 0; +export const UNSET_CONFIG_ITEM = '--'; export type EsIndexName = string; export type DependentVariable = string; @@ -47,6 +49,7 @@ export interface State { advancedEditorMessages: FormMessage[]; advancedEditorRawString: string; form: { + computeFeatureInfluence: string; createIndexPattern: boolean; dependentVariable: DependentVariable; dependentVariableFetchFail: boolean; @@ -57,31 +60,47 @@ export interface State { destinationIndexNameEmpty: boolean; destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; + eta: undefined | number; excludes: string[]; + excludesTableItems: FieldSelectionItem[]; excludesOptions: EuiComboBoxOptionOption[]; + featureBagFraction: undefined | number; + featureInfluenceThreshold: undefined | number; fieldOptionsFetchFail: boolean; + gamma: undefined | number; jobId: DataFrameAnalyticsId; jobIdExists: boolean; jobIdEmpty: boolean; jobIdInvalidMaxLength: boolean; jobIdValid: boolean; jobType: AnalyticsJobType; + jobConfigQuery: any; + jobConfigQueryString: string | undefined; + lambda: number | undefined; loadingDepVarOptions: boolean; loadingFieldOptions: boolean; maxDistinctValuesError: string | undefined; + maxTrees: undefined | number; + method: undefined | string; modelMemoryLimit: string | undefined; modelMemoryLimitUnitValid: boolean; modelMemoryLimitValidationResult: any; + nNeighbors: undefined | number; numTopFeatureImportanceValues: number | undefined; numTopFeatureImportanceValuesValid: boolean; + numTopClasses: number; + outlierFraction: undefined | number; + predictionFieldName: undefined | string; previousJobType: null | AnalyticsJobType; previousSourceIndex: EsIndexName | undefined; requiredFieldsError: string | undefined; + randomizeSeed: undefined | number; sourceIndex: EsIndexName; sourceIndexNameEmpty: boolean; sourceIndexNameValid: boolean; sourceIndexContainsNumericalFields: boolean; sourceIndexFieldsCheckFailed: boolean; + standardizationEnabled: undefined | string; trainingPercent: number; }; disabled: boolean; @@ -105,7 +124,8 @@ export const getInitialState = (): State => ({ advancedEditorMessages: [], advancedEditorRawString: '', form: { - createIndexPattern: false, + computeFeatureInfluence: 'true', + createIndexPattern: true, dependentVariable: '', dependentVariableFetchFail: false, dependentVariableOptions: [], @@ -115,8 +135,13 @@ export const getInitialState = (): State => ({ destinationIndexNameEmpty: true, destinationIndexNameValid: false, destinationIndexPatternTitleExists: false, + eta: undefined, excludes: [], + featureBagFraction: undefined, + featureInfluenceThreshold: undefined, fieldOptionsFetchFail: false, + gamma: undefined, + excludesTableItems: [], excludesOptions: [], jobId: '', jobIdExists: false, @@ -124,22 +149,33 @@ export const getInitialState = (): State => ({ jobIdInvalidMaxLength: false, jobIdValid: false, jobType: undefined, + jobConfigQuery: { match_all: {} }, + jobConfigQueryString: undefined, + lambda: undefined, loadingDepVarOptions: false, loadingFieldOptions: false, maxDistinctValuesError: undefined, + maxTrees: undefined, + method: undefined, modelMemoryLimit: undefined, modelMemoryLimitUnitValid: true, modelMemoryLimitValidationResult: null, + nNeighbors: undefined, numTopFeatureImportanceValues: DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, numTopFeatureImportanceValuesValid: true, + numTopClasses: 2, + outlierFraction: undefined, + predictionFieldName: undefined, previousJobType: null, previousSourceIndex: undefined, requiredFieldsError: undefined, + randomizeSeed: undefined, sourceIndex: '', sourceIndexNameEmpty: true, sourceIndexNameValid: false, sourceIndexContainsNumericalFields: true, sourceIndexFieldsCheckFailed: false, + standardizationEnabled: 'true', trainingPercent: 80, }, jobConfig: {}, @@ -222,6 +258,7 @@ export const getJobConfigFromFormState = ( index: formState.sourceIndex.includes(',') ? formState.sourceIndex.split(',').map((d) => d.trim()) : formState.sourceIndex, + query: formState.jobConfigQuery, }, dest: { index: formState.destinationIndex, @@ -239,15 +276,55 @@ export const getJobConfigFromFormState = ( formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION ) { - jobConfig.analysis = { - [formState.jobType]: { - dependent_variable: formState.dependentVariable, - num_top_feature_importance_values: formState.numTopFeatureImportanceValues, - training_percent: formState.trainingPercent, + let analysis = { + dependent_variable: formState.dependentVariable, + num_top_feature_importance_values: formState.numTopFeatureImportanceValues, + training_percent: formState.trainingPercent, + }; + + analysis = Object.assign( + analysis, + formState.predictionFieldName && { prediction_field_name: formState.predictionFieldName }, + formState.eta && { eta: formState.eta }, + formState.featureBagFraction && { + feature_bag_fraction: formState.featureBagFraction, }, + formState.gamma && { gamma: formState.gamma }, + formState.lambda && { lambda: formState.lambda }, + formState.maxTrees && { max_trees: formState.maxTrees }, + formState.randomizeSeed && { randomize_seed: formState.randomizeSeed } + ); + + jobConfig.analysis = { + [formState.jobType]: analysis, }; } + if ( + formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + jobConfig?.analysis?.classification !== undefined && + formState.numTopClasses !== undefined + ) { + // @ts-ignore + jobConfig.analysis.classification.num_top_classes = formState.numTopClasses; + } + + if (formState.jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) { + const analysis = Object.assign( + {}, + formState.method && { method: formState.method }, + formState.nNeighbors && { + n_neighbors: formState.nNeighbors, + }, + formState.outlierFraction && { outlier_fraction: formState.outlierFraction }, + formState.standardizationEnabled && { + standardization_enabled: formState.standardizationEnabled, + } + ); + // @ts-ignore + jobConfig.analysis.outlier_detection = analysis; + } + return jobConfig; }; @@ -279,6 +356,11 @@ export function getCloneFormStateFromJobConfig( resultState.dependentVariable = analysisConfig.dependent_variable; resultState.numTopFeatureImportanceValues = analysisConfig.num_top_feature_importance_values; resultState.trainingPercent = analysisConfig.training_percent; + + if (isClassificationAnalysis(analyticsJobConfig.analysis)) { + // @ts-ignore + resultState.numTopClasses = analysisConfig.num_top_classes; + } } return resultState; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 1ec767d014a2e..c4cbe149f88bc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -36,11 +36,24 @@ import { getCloneFormStateFromJobConfig, } from './state'; +import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; + +export interface AnalyticsCreationStep { + number: ANALYTICS_STEPS; + completed: boolean; +} + export interface CreateAnalyticsFormProps { actions: ActionDispatchers; state: State; } +export interface CreateAnalyticsStepProps extends CreateAnalyticsFormProps { + setCurrentStep: React.Dispatch>; + step?: ANALYTICS_STEPS; + stepActivated?: boolean; +} + export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const mlContext = useMlContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); @@ -261,8 +274,12 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.OPEN_MODAL }); }; + const initiateWizard = async () => { + await mlContext.indexPatterns.clearCache(); + await prepareFormValidation(); + }; + const startAnalyticsJob = async () => { - setIsModalButtonDisabled(true); try { const response = await ml.dataFrameAnalytics.startDataFrameAnalytics(jobId); if (response.acknowledged !== true) { @@ -278,7 +295,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { ), }); setIsJobStarted(true); - setIsModalButtonDisabled(false); refresh(); } catch (e) { addRequestMessage({ @@ -290,7 +306,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { } ), }); - setIsModalButtonDisabled(false); } }; @@ -331,6 +346,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { closeModal, createAnalyticsJob, openModal, + initiateWizard, resetAdvancedEditorMessages, setAdvancedEditorRawString, setFormState, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 126fd25a536f6..fd86d9f48f46d 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -182,10 +182,7 @@ export const DatavisualizerSelector: FC = () => { } description={startTrialDescription()} footer={ - + = ({ /> } description="" - href={`${basePath.get()}/app/kibana#/management/data/index_management/indices/filter/${index}`} + href={`${basePath.get()}/app/management/data/index_management/indices/filter/${index}`} /> @@ -153,7 +153,7 @@ export const ResultsLinks: FC = ({ /> } description="" - href={`${basePath.get()}/app/kibana#/management/kibana/indexPatterns${ + href={`${basePath.get()}/app/management/kibana/indexPatterns${ createIndexPattern ? `/patterns/${indexPatternId}` : '' }`} /> diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index d68c0342ac857..97b4043c9fd64 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -27,6 +27,7 @@ import { Query, esQuery, esKuery, + UI_SETTINGS, } from '../../../../../../../src/plugins/data/public'; import { SavedSearchSavedObject } from '../../../../common/types/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; @@ -254,7 +255,7 @@ export const Page: FC = () => { qry = esKuery.toElasticsearchQuery(ast, currentIndexPattern); } else { qry = esQuery.luceneStringToDsl(qryString); - esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); + esQuery.decorateQuery(qry, kibanaConfig.get(UI_SETTINGS.QUERY_STRING_OPTIONS)); } return { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 6c7c3e9040216..7a18914957ba9 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -25,6 +25,7 @@ import { getTickValues, numTicksForDateFormat, removeLabelOverlap, + chartExtendedLimits, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; import { getTimeBucketsFromCache } from '../../util/time_buckets'; @@ -98,7 +99,7 @@ export class ExplorerChartDistribution extends React.Component { const filteredChartData = init(config); drawRareChart(filteredChartData); - function init({ chartData }) { + function init({ chartData, functionDescription }) { const $el = $('.ml-explorer-chart'); // Clear any existing elements from the visualization, @@ -137,22 +138,24 @@ export class ExplorerChartDistribution extends React.Component { }); if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { - const focusData = chartData - .filter((d) => { - return d.entity === highlight; - }) - .map((d) => d.value); - const focusExtent = d3.extent(focusData); - + const focusData = chartData.filter((d) => { + return d.entity === highlight; + }); + // calculate the max y domain based on value, typical, and actual + // also sets the min to be at least 0 if the series function type is `count` + const { min: yScaleDomainMin, max: yScaleDomainMax } = chartExtendedLimits( + focusData, + functionDescription + ); // now again filter chartData to include only the data points within the domain chartData = chartData.filter((d) => { - return d.value <= focusExtent[1]; + return d.value <= yScaleDomainMax; }); lineChartYScale = d3.scale .linear() .range([chartHeight, 0]) - .domain([0, focusExtent[1]]) + .domain([yScaleDomainMin < 0 ? yScaleDomainMin : 0, yScaleDomainMax]) .nice(); } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { // avoid overflowing the border of the highlighted area diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index c2dd8e6378305..8b30dccc25306 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -518,7 +518,6 @@ function processRecordsForDisplay(anomalyRecords) { } }); - console.log('explorer charts aggregatedData is:', aggregatedData); let recordsForSeries = []; // Convert to an array of the records with the highest record_score per unique series. _.each(aggregatedData, (detectorsForJob) => { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index f35a000b7f9e1..bd6a7ee59c942 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -715,7 +715,6 @@ export async function loadDataForCharts( (selectedCells !== undefined && Object.keys(selectedCells).length > 0) || influencersFilterQuery !== undefined ) { - console.log('Explorer anomaly charts data set:', resp.records); resolve(resp.records); } @@ -764,7 +763,6 @@ export function loadOverallData(selectedJobs, interval, bounds) { interval.asSeconds() ); - console.log('Explorer overall swimlane data set:', overallSwimlaneData); resolve({ loading: false, overallSwimlaneData, @@ -795,7 +793,6 @@ export function loadViewBySwimlane( getSwimlaneContainerWidth(noInfluencersConfigured) ).asSeconds() ); - console.log('Explorer view by swimlane data set:', viewBySwimlaneData); resolve({ viewBySwimlaneData, @@ -879,7 +876,6 @@ export async function loadTopInfluencers( ) .then((resp) => { // TODO - sort the influencers keys so that the partition field(s) are first. - console.log('Explorer top influencers data set:', resp.influencers); resolve(resp.influencers); }); } else { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js index 67de83e90695d..eeff91be130ea 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js @@ -167,7 +167,7 @@ class CreateWatchService { saveWatch(watchModel) .then(() => { this.status.watch = this.STATUS.SAVED; - this.config.watcherEditURL = `${basePath.get()}/app/kibana#/management/insightsAndAlerting/watcher/watches/watch/${id}/edit?_g=()`; + this.config.watcherEditURL = `${basePath.get()}/app/management/insightsAndAlerting/watcher/watches/watch/${id}/edit?_g=()`; resolve({ id, url: this.config.watcherEditURL, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 50e5aeeb29dd9..8f89c4a049189 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -133,7 +133,7 @@ export function extractJobDetails(job) { defaultMessage: 'Datafeed', }), position: 'left', - items: filterObjects(job.datafeed_config, true, true), + items: filterObjects(job.datafeed_config || {}, true, true), }; if (job.node) { datafeed.items.push(['node', JSON.stringify(job.node)]); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 0375997b86bb8..56da4f1e0ff84 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -125,7 +125,7 @@ export class JobDetails extends Component { }, ]; - if (showFullDetails) { + if (showFullDetails && datafeed.items.length) { // Datafeed should be at index 2 in tabs array for full details tabs.splice(2, 0, { id: 'datafeed', diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts index cfe37ce14bb78..5d1fc6f0a3c92 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts +++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts @@ -11,12 +11,14 @@ import { ManagementAppMountParams } from '../../../../../../../src/plugins/manag import { MlStartDependencies } from '../../../plugin'; import { JobsListPage } from './components'; import { getJobsListBreadcrumbs } from '../breadcrumbs'; +import { setDependencyCache, clearCache } from '../../util/dependency_cache'; const renderApp = (element: HTMLElement, coreStart: CoreStart) => { const I18nContext = coreStart.i18n.Context; ReactDOM.render(React.createElement(JobsListPage, { I18nContext }), element); return () => { unmountComponentAtNode(element); + clearCache(); }; }; @@ -25,6 +27,15 @@ export async function mountApp( params: ManagementAppMountParams ) { const [coreStart] = await core.getStartServices(); + + setDependencyCache({ + docLinks: coreStart.docLinks!, + basePath: coreStart.http.basePath, + http: coreStart.http, + i18n: coreStart.i18n, + }); + params.setBreadcrumbs(getJobsListBreadcrumbs()); + return renderApp(params.element, coreStart); } diff --git a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx index 87a7156b6f52e..119346ec8035a 100644 --- a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx @@ -42,7 +42,7 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled } const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const docsLink = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-ml.html`; - const transformsLink = `${basePath.get()}/app/kibana#/management/data/transform`; + const transformsLink = `${basePath.get()}/app/management/data/transform`; return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx new file mode 100644 index 0000000000000..68af9a2a49cab --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -0,0 +1,41 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { + defaultMessage: 'Data Frame Analytics', + }), + href: '#/data_frame_analytics', + }, +]; + +export const analyticsJobsCreationRoute: MlRoute = { + path: '/data_frame_analytics/new_job', + render: (props, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, deps }) => { + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); + const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts index 552c15a408b65..9b6bcc25c8c7e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts @@ -6,3 +6,4 @@ export * from './analytics_jobs_list'; export * from './analytics_job_exploration'; +export * from './analytics_job_creation'; diff --git a/x-pack/plugins/ml/public/application/services/explorer_service.ts b/x-pack/plugins/ml/public/application/services/explorer_service.ts index efcec8cf2b954..717ed3ba64c37 100644 --- a/x-pack/plugins/ml/public/application/services/explorer_service.ts +++ b/x-pack/plugins/ml/public/application/services/explorer_service.ts @@ -6,7 +6,11 @@ import { IUiSettingsClient } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { TimefilterContract, TimeRange } from '../../../../../../src/plugins/data/public'; +import { + TimefilterContract, + TimeRange, + UI_SETTINGS, +} from '../../../../../../src/plugins/data/public'; import { getBoundsRoundedToInterval, TimeBuckets, TimeRangeBounds } from '../util/time_buckets'; import { ExplorerJob, OverallSwimlaneData, SwimlaneData } from '../explorer/explorer_utils'; import { VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants'; @@ -25,8 +29,8 @@ export class ExplorerService { private mlResultsService: MlResultsService ) { this.timeBuckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 8d59270b36dae..a3be479571702 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -368,6 +368,7 @@ class JobService { delete tempJob.calendars; delete tempJob.timing_stats; delete tempJob.forecasts_stats; + delete tempJob.assignment_explanation; delete tempJob.analysis_config.use_per_partition_normalization; @@ -411,8 +412,7 @@ class JobService { // return the promise chain return ml .updateJob({ jobId, job }) - .then((resp) => { - console.log('update job', resp); + .then(() => { return { success: true }; }) .catch((err) => { @@ -422,7 +422,7 @@ class JobService { values: { jobId }, }) ); - console.log('update job', err); + console.error('update job', err); return { success: false, message: err.message }; }); } diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 2caf964cb9774..4ec7c5cb6d819 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -65,6 +65,44 @@ export function chartLimits(data = []) { return limits; } +export function chartExtendedLimits(data = [], functionDescription) { + let _min = Infinity; + let _max = -Infinity; + data.forEach((d) => { + let metricValue = d.value; + const actualValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; + const typicalValue = Array.isArray(d.typical) ? d.typical[0] : d.typical; + + if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) { + // If an anomaly coincides with a gap in the data, use the anomaly actual value. + metricValue = actualValue; + } + + if (d.anomalyScore !== undefined) { + _min = Math.min(_min, metricValue, actualValue, typicalValue); + _max = Math.max(_max, metricValue, actualValue, typicalValue); + } else { + _min = Math.min(_min, metricValue); + _max = Math.max(_max, metricValue); + } + }); + const limits = { max: _max, min: _min }; + + // add padding of 5% of the difference between max and min + // if we ended up with the same value for both of them + if (limits.max === limits.min) { + const padding = limits.max * 0.05; + limits.max += padding; + limits.min -= padding; + } + + // makes sure the domain starts at 0 if the aggregation is by count + // since the number should always be positive + if (functionDescription === 'count' && limits.min < 0) { + limits.min = 0; + } + return limits; +} export function drawLineChartDots(data, lineChartGroup, lineChartValuesLine, radius = 1.5) { // We need to do this because when creating a line for a chart which has data gaps, // if there are single datapoints without any valid data before and after them, diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts index 9c5d663a6e4ab..91661ea3fd3fa 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts +++ b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts @@ -5,6 +5,7 @@ */ import { Moment } from 'moment'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; export interface TimeRangeBounds { min?: Moment; diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.js b/x-pack/plugins/ml/public/application/util/time_buckets.js index 2b23eca1ab5c0..1915a4ce6516b 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.js +++ b/x-pack/plugins/ml/public/application/util/time_buckets.js @@ -11,7 +11,7 @@ import dateMath from '@elastic/datemath'; import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; import { parseInterval } from '../../../common/util/parse_interval'; import { getFieldFormats, getUiSettings } from './dependency_cache'; -import { FIELD_FORMAT_IDS } from '../../../../../../src/plugins/data/public'; +import { FIELD_FORMAT_IDS, UI_SETTINGS } from '../../../../../../src/plugins/data/public'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. @@ -21,8 +21,8 @@ const calcAuto = timeBucketsCalcAutoIntervalProvider(); export function getTimeBucketsFromCache() { const uiSettings = getUiSettings(); return new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); @@ -35,8 +35,8 @@ export function getTimeBucketsFromCache() { */ export function TimeBuckets(timeBucketsConfig) { this._timeBucketsConfig = timeBucketsConfig; - this.barTarget = this._timeBucketsConfig['histogram:barTarget']; - this.maxBars = this._timeBucketsConfig['histogram:maxBars']; + this.barTarget = this._timeBucketsConfig[UI_SETTINGS.HISTOGRAM_BAR_TARGET]; + this.maxBars = this._timeBucketsConfig[UI_SETTINGS.HISTOGRAM_MAX_BARS]; } /** diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.test.js b/x-pack/plugins/ml/public/application/util/time_buckets.test.js index baecf7df90641..250c7255f5b99 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.test.js +++ b/x-pack/plugins/ml/public/application/util/time_buckets.test.js @@ -5,6 +5,7 @@ */ import moment from 'moment'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { TimeBuckets, getBoundsRoundedToInterval, calcEsInterval } from './time_buckets'; describe('ML - time buckets', () => { @@ -13,8 +14,8 @@ describe('ML - time buckets', () => { beforeEach(() => { const timeBucketsConfig = { - 'histogram:maxBars': 100, - 'histogram:barTarget': 50, + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: 100, + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: 50, }; autoBuckets = new TimeBuckets(timeBucketsConfig); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index e48ce9f9a1d92..74a7b432de267 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -27,7 +27,7 @@ import { MlStartDependencies } from '../../plugin'; import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters'; import { Query } from '../../../../../../src/plugins/data/common/query'; -import { esKuery } from '../../../../../../src/plugins/data/public'; +import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { ExplorerJob, OverallSwimlaneData } from '../../application/explorer/explorer_utils'; import { parseInterval } from '../../../common/util/parse_interval'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; @@ -62,8 +62,8 @@ export function useSwimlaneInputResolver( const timeBuckets = useMemo(() => { return new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 88b86de322e3c..de393e002c55b 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -77,6 +77,12 @@ export const analysisConfigSchema = schema.object({ detectors: schema.arrayOf(detectorSchema), influencers: schema.arrayOf(schema.maybe(schema.string())), categorization_field_name: schema.maybe(schema.string()), + per_partition_categorization: schema.maybe( + schema.object({ + enabled: schema.boolean(), + stop_on_warn: schema.maybe(schema.boolean()), + }) + ), }); export const anomalyDetectionJobSchema = { diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 0b2469c103578..e6b4e4ccf8582 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -47,6 +47,7 @@ export const dataAnalyticsExplainSchema = schema.object({ /** Source */ source: schema.object({ index: schema.string(), + query: schema.maybe(schema.any()), }), analysis: schema.any(), analyzed_fields: schema.maybe(schema.any()), diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js index 263da16340cda..f1a867536b606 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -288,7 +288,7 @@ const handleClickIncompatibleLicense = (scope, clusterName) => { }; const handleClickInvalidLicense = (scope, clusterName) => { - const licensingPath = `${Legacy.shims.getBasePath()}/app/kibana#/management/stack/license_management/home`; + const licensingPath = `${Legacy.shims.getBasePath()}/app/management/stack/license_management/home`; licenseWarning(scope, { title: toMountPoint( diff --git a/x-pack/plugins/monitoring/public/components/license/index.js b/x-pack/plugins/monitoring/public/components/license/index.js index e8ea1f8df227a..076b8e6d543e6 100644 --- a/x-pack/plugins/monitoring/public/components/license/index.js +++ b/x-pack/plugins/monitoring/public/components/license/index.js @@ -169,7 +169,7 @@ const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { export function License(props) { const { status, type, isExpired, expiryDate } = props; - const licenseManagement = `${Legacy.shims.getBasePath()}/app/kibana#/management/stack/license_management`; + const licenseManagement = `${Legacy.shims.getBasePath()}/app/management/stack/license_management`; return ( diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 73133da5a006c..24383028e558c 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -17,6 +17,7 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; +import { UI_SETTINGS } from '../../../../src/plugins/data/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MonitoringPluginDependencies, MonitoringConfig } from './types'; import { @@ -110,7 +111,7 @@ export class MonitoringPlugin timefilter.setRefreshInterval(refreshInterval); timefilter.setTime(time); uiSettings.overrideLocalDefault( - 'timepicker:refreshIntervalDefaults', + UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS, JSON.stringify(refreshInterval) ); uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.html b/x-pack/plugins/monitoring/public/views/access_denied/index.html index 63cd4440ecf8a..24863559212f7 100644 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.html +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.html @@ -30,12 +30,13 @@
diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.js b/x-pack/plugins/monitoring/public/views/access_denied/index.js index 856e59702963a..f7a4d03a26452 100644 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.js +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.js @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash'; +import { kbnBaseUrl } from '../../../../../../src/plugins/kibana_legacy/common/kbn_base_url'; import { uiRoutes } from '../../angular/helpers/routes'; -import { Legacy } from '../../legacy_shims'; import template from './index.html'; const tryPrivilege = ($http, kbnUrl) => { return $http .get('../api/monitoring/v1/check_access') .then(() => kbnUrl.redirect('/home')) - .catch(noop); + .catch(() => true); }; uiRoutes.when('/access-denied', { @@ -31,17 +30,13 @@ uiRoutes.when('/access-denied', { }, }, controllerAs: 'accessDenied', - controller($scope, $injector) { - const $window = $injector.get('$window'); - const kbnBaseUrl = $injector.get('kbnBaseUrl'); + controller: function ($scope, $injector) { const $http = $injector.get('$http'); const kbnUrl = $injector.get('kbnUrl'); const $interval = $injector.get('$interval'); // The template's "Back to Kibana" button click handler - this.goToKibana = () => { - $window.location.href = Legacy.shims.getBasePath() + kbnBaseUrl; - }; + this.goToKibanaURL = kbnBaseUrl; // keep trying to load data in the background const accessPoller = $interval(() => tryPrivilege($http, kbnUrl), 5 * 1000); // every 5 seconds diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx new file mode 100644 index 0000000000000..21a9fabf445f1 --- /dev/null +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; +import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; +import { Home } from '../pages/home'; +import { PluginContext } from '../context/plugin_context'; + +export const renderApp = (core: CoreStart, { element }: AppMountParameters) => { + const i18nCore = core.i18n; + const isDarkMode = core.uiSettings.get('theme:darkMode'); + ReactDOM.render( + + + + + + + , + element + ); + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/observability/public/assets/observability_overview.png b/x-pack/plugins/observability/public/assets/observability_overview.png new file mode 100644 index 0000000000000..70be08af9745a Binary files /dev/null and b/x-pack/plugins/observability/public/assets/observability_overview.png differ diff --git a/x-pack/plugins/observability/public/context/plugin_context.tsx b/x-pack/plugins/observability/public/context/plugin_context.tsx new file mode 100644 index 0000000000000..7d705e7a6cc05 --- /dev/null +++ b/x-pack/plugins/observability/public/context/plugin_context.tsx @@ -0,0 +1,14 @@ +/* + * 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 { createContext } from 'react'; +import { AppMountContext } from 'kibana/public'; + +export interface PluginContextValue { + core: AppMountContext['core']; +} + +export const PluginContext = createContext({} as PluginContextValue); diff --git a/x-pack/plugins/observability/public/hooks/use_plugin_context.tsx b/x-pack/plugins/observability/public/hooks/use_plugin_context.tsx new file mode 100644 index 0000000000000..eeec115f0d286 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_plugin_context.tsx @@ -0,0 +1,12 @@ +/* + * 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 { useContext } from 'react'; +import { PluginContext } from '../context/plugin_context'; + +export function usePluginContext() { + return useContext(PluginContext); +} diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx new file mode 100644 index 0000000000000..b9b567bef4ab4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -0,0 +1,205 @@ +/* + * 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 { + EuiButton, + EuiCard, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiImage, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { appsSection, tryItOutItemsSection } from './section'; + +const Container = styled.div` + min-height: calc(100vh - 48px); + background: ${(props) => props.theme.eui.euiColorEmptyShade}; +`; + +const Title = styled.div` + background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; + border-bottom: ${(props) => props.theme.eui.euiBorderThin}; +`; + +const Page = styled.div` + width: 100%; + max-width: 1200px; + margin: 0 auto; + overflow: hidden; +} +`; + +const EuiCardWithoutPadding = styled(EuiCard)` + padding: 0; +`; + +export const Home = () => { + const { core } = usePluginContext(); + + useEffect(() => { + core.chrome.setBreadcrumbs([ + { + text: i18n.translate('xpack.observability.home.breadcrumb.observability', { + defaultMessage: 'Observability', + }), + }, + { + text: i18n.translate('xpack.observability.home.breadcrumb.gettingStarted', { + defaultMessage: 'Getting started', + }), + }, + ]); + }, [core]); + + return ( + + + <Page> + <EuiSpacer size="xxl" /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiIcon type="logoObservability" size="xxl" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size="m"> + <h1> + {i18n.translate('xpack.observability.home.title', { + defaultMessage: 'Observability', + })} + </h1> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="xxl" /> + </Page> + + + + + {/* title and description */} + + +

+ {i18n.translate('xpack.observability.home.sectionTitle', { + defaultMessage: 'Observability built on the Elastic Stack', + })} +

+
+ + + {i18n.translate('xpack.observability.home.sectionsubtitle', { + defaultMessage: + 'Bring your logs, metrics, and APM traces together at scale in a single stack so you can monitor and react to events happening anywhere in your environment.', + })} + +
+ + {/* Apps sections */} + + + + + + {appsSection.map((app) => ( + + } + title={ + +

{app.title}

+
+ } + description={app.description} + /> +
+ ))} +
+
+ + + +
+
+ + {/* Get started button */} + + + + + {i18n.translate('xpack.observability.home.getStatedButton', { + defaultMessage: 'Get started', + })} + + + + + + + + {/* Try it out */} + + + + +

+ {i18n.translate('xpack.observability.home.tryItOut', { + defaultMessage: 'Try it out', + })} +

+
+
+
+
+ + {/* Try it out sections */} + + + {tryItOutItemsSection.map((item) => ( + + } + title={ + +

{item.title}

+
+ } + description={item.description} + target={item.target} + href={item.href} + /> +
+ ))} +
+ +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts new file mode 100644 index 0000000000000..f8bbfbfa30548 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/home/section.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +interface ISection { + id: string; + title: string; + icon: string; + description: string; + href?: string; + target?: '_blank'; +} + +export const appsSection: ISection[] = [ + { + id: 'logs', + title: i18n.translate('xpack.observability.section.apps.logs.title', { + defaultMessage: 'Logs', + }), + icon: 'logoLogging', + description: i18n.translate('xpack.observability.section.apps.logs.description', { + defaultMessage: + 'The Elastic Stack (sometimes known as the ELK Stack) is the most popular open source logging platform.', + }), + }, + { + id: 'apm', + title: i18n.translate('xpack.observability.section.apps.apm.title', { + defaultMessage: 'APM', + }), + icon: 'logoAPM', + description: i18n.translate('xpack.observability.section.apps.apm.description', { + defaultMessage: + 'See exactly where your application is spending time so you can quickly fix issues and feel good about the code you push.', + }), + }, + { + id: 'metrics', + title: i18n.translate('xpack.observability.section.apps.metrics.title', { + defaultMessage: 'Metrics', + }), + icon: 'logoMetrics', + description: i18n.translate('xpack.observability.section.apps.metrics.description', { + defaultMessage: + 'Already using the Elastic Stack for logs? Add metrics in just a few steps and correlate metrics and logs in one place.', + }), + }, + { + id: 'uptime', + title: i18n.translate('xpack.observability.section.apps.uptime.title', { + defaultMessage: 'Uptime', + }), + icon: 'logoUptime', + description: i18n.translate('xpack.observability.section.apps.uptime.description', { + defaultMessage: + 'React to availability issues across your apps and services before they affect users.', + }), + }, +]; + +export const tryItOutItemsSection: ISection[] = [ + { + id: 'demo', + title: i18n.translate('xpack.observability.section.tryItOut.demo.title', { + defaultMessage: 'Demo Playground', + }), + icon: 'play', + description: '', + href: 'https://demo.elastic.co/', + target: '_blank', + }, + { + id: 'sampleData', + title: i18n.translate('xpack.observability.section.tryItOut.sampleData.title', { + defaultMessage: 'Add sample data', + }), + icon: 'documents', + description: '', + href: '/app/home#/tutorial_directory/sampleData', + }, +]; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index a7eb1c50a0392..f2c88a7b1c056 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -3,13 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Plugin as PluginClass, PluginInitializerContext } from 'kibana/public'; +import { + AppMountParameters, + CoreSetup, + DEFAULT_APP_CATEGORIES, + Plugin as PluginClass, + PluginInitializerContext, +} from '../../../../src/core/public'; export type ClientSetup = void; export type ClientStart = void; -export class Plugin implements PluginClass { +export class Plugin implements PluginClass { constructor(context: PluginInitializerContext) {} - start() {} - setup() {} + + public setup(core: CoreSetup) { + core.application.register({ + id: 'observability-overview', + title: 'Overview', + order: 8000, + appRoute: '/app/observability', + category: DEFAULT_APP_CATEGORIES.observability, + + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services + const [coreStart] = await core.getStartServices(); + + return renderApp(coreStart, params); + }, + }); + } + public start() {} } diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx index 5a417933e8022..b707fd493bcdd 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx @@ -14,7 +14,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { monaco } from '@kbn/ui-shared-deps/monaco'; +import { monaco } from '@kbn/monaco'; import { i18n } from '@kbn/i18n'; import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts b/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts index 36cd4f280ac4c..d0426a5e67e46 100644 --- a/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts +++ b/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts @@ -19,6 +19,7 @@ export const useSubmitCode = (http: HttpSetup) => { const [response, setResponse] = useState(undefined); const [inProgress, setInProgress] = useState(false); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const submit = useCallback( debounce( async (config: Payload) => { diff --git a/x-pack/plugins/painless_lab/public/lib/monaco_painless_lang.ts b/x-pack/plugins/painless_lab/public/lib/monaco_painless_lang.ts index 602697064a768..4278c77b4c791 100644 --- a/x-pack/plugins/painless_lab/public/lib/monaco_painless_lang.ts +++ b/x-pack/plugins/painless_lab/public/lib/monaco_painless_lang.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as monaco from 'monaco-editor'; +import { monaco } from '@kbn/monaco'; /** * Extends the default type for a Monarch language so we can use diff --git a/x-pack/plugins/painless_lab/public/services/language_service.ts b/x-pack/plugins/painless_lab/public/services/language_service.ts index efff9cd0e78d5..68ac3ca290ad8 100644 --- a/x-pack/plugins/painless_lab/public/services/language_service.ts +++ b/x-pack/plugins/painless_lab/public/services/language_service.ts @@ -7,7 +7,7 @@ // It is important that we use this specific monaco instance so that // editor settings are registered against the instance our React component // uses. -import { monaco } from '@kbn/ui-shared-deps/monaco'; +import { monaco } from '@kbn/monaco'; // @ts-ignore import workerSrc from 'raw-loader!monaco-editor/min/vs/base/worker/workerMain.js'; diff --git a/x-pack/plugins/remote_clusters/public/application/app.js b/x-pack/plugins/remote_clusters/public/application/app.js index 483b2f5b97e27..714887b039a42 100644 --- a/x-pack/plugins/remote_clusters/public/application/app.js +++ b/x-pack/plugins/remote_clusters/public/application/app.js @@ -6,9 +6,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Switch, Route, Redirect, withRouter } from 'react-router-dom'; +import { Switch, Route, Redirect, Router } from 'react-router-dom'; -import { CRUD_APP_BASE_PATH, UIM_APP_LOAD } from './constants'; +import { UIM_APP_LOAD } from './constants'; import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; import { RemoteClusterList, RemoteClusterAdd, RemoteClusterEdit } from './sections'; @@ -22,15 +22,16 @@ class AppComponent extends Component { constructor(...args) { super(...args); + setUserHasLeftApp(false); this.registerRouter(); } registerRouter() { // Share the router with the app without requiring React or context. - const { history, location } = this.props; + const { history } = this.props; registerRouter({ history, - route: { location }, + route: { location: history.location }, }); } @@ -45,17 +46,16 @@ class AppComponent extends Component { render() { return ( -
+ - - - - - + + + + -
+ ); } } -export const App = withRouter(AppComponent); +export const App = AppComponent; diff --git a/x-pack/plugins/remote_clusters/public/application/index.d.ts b/x-pack/plugins/remote_clusters/public/application/index.d.ts index b021dca51bacd..8b2af65e4fff7 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.d.ts +++ b/x-pack/plugins/remote_clusters/public/application/index.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ScopedHistory } from 'kibana/public'; import { RegisterManagementAppArgs, I18nStart } from '../types'; export declare const renderApp: ( @@ -11,5 +12,6 @@ export declare const renderApp: ( I18nContext: I18nStart['Context'], appDependencies: { isCloudEnabled?: boolean; - } + }, + history: ScopedHistory ) => ReturnType; diff --git a/x-pack/plugins/remote_clusters/public/application/index.js b/x-pack/plugins/remote_clusters/public/application/index.js index cf6e855ba58df..25e171c9ef51d 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.js +++ b/x-pack/plugins/remote_clusters/public/application/index.js @@ -6,7 +6,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HashRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { App } from './app'; @@ -15,14 +14,12 @@ import { AppContextProvider } from './app_context'; import './_hacks.scss'; -export const renderApp = (elem, I18nContext, appDependencies) => { +export const renderApp = (elem, I18nContext, appDependencies, history) => { render( - - - + , diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index f5053e3e18ccf..b13e833f60b18 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageContent } from '@elastic/eui'; -import { CRUD_APP_BASE_PATH } from '../../constants'; import { getRouter, redirect, extractQueryParams } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterPageTitle, RemoteClusterForm } from '../components'; @@ -49,7 +48,7 @@ export class RemoteClusterAdd extends PureComponent { const decodedRedirect = decodeURIComponent(redirectUrl); redirect(decodedRedirect); } else { - history.push(CRUD_APP_BASE_PATH); + history.push('/list'); } }; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js index 5e3b2f12a57fd..9018647600b8d 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js @@ -20,8 +20,8 @@ import { EuiTextColor, } from '@elastic/eui'; -import { CRUD_APP_BASE_PATH } from '../../constants'; -import { extractQueryParams, getRouter, getRouterLinkProps, redirect } from '../../services'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import { extractQueryParams, getRouter, redirect } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components'; @@ -89,7 +89,7 @@ export class RemoteClusterEdit extends Component { const decodedRedirect = decodeURIComponent(redirectUrl); redirect(decodedRedirect); } else { - history.push(CRUD_APP_BASE_PATH); + history.push('/list'); openDetailPanel(clusterName); } }; @@ -143,7 +143,7 @@ export class RemoteClusterEdit extends Component { diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js index 22c986c203a04..03be45c760244 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js @@ -30,12 +30,11 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; - -import { CRUD_APP_BASE_PATH } from '../../../constants'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { PROXY_MODE } from '../../../../../common/constants'; -import { getRouterLinkProps } from '../../../services'; import { ConfiguredByNodeWarning } from '../../components'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; +import { getRouter } from '../../../services'; import { proxyModeUrl } from '../../../services/documentation'; export class DetailPanel extends Component { @@ -114,7 +113,8 @@ export class DetailPanel extends Component { renderClusterWithDeprecatedSettingWarning( { hasDeprecatedProxySetting, isConfiguredByNode }, - clusterName + clusterName, + history ) { if (!hasDeprecatedProxySetting) { return null; @@ -156,7 +156,7 @@ export class DetailPanel extends Component { defaultMessage="{editLink} to update the settings." values={{ editLink: ( - + {this.renderClusterConfiguredByNodeWarning(cluster)} - {this.renderClusterWithDeprecatedSettingWarning(cluster, clusterName)} + {this.renderClusterWithDeprecatedSettingWarning(cluster, clusterName, history)} {this.renderCluster(cluster)} )} @@ -465,7 +465,7 @@ export class DetailPanel extends Component { ); } - renderFlyoutFooter() { + renderFlyoutFooter(history) { const { cluster, clusterName, closeDetailPanel } = this.props; return ( @@ -507,7 +507,7 @@ export class DetailPanel extends Component { - {this.renderFlyoutBody()} + {this.renderFlyoutBody(history)} - {this.renderFlyoutFooter()} + {this.renderFlyoutFooter(history)} ); } diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js index 207aa8045c011..6d40cbbeb82ae 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js @@ -28,8 +28,8 @@ import { EuiCallOut, } from '@elastic/eui'; -import { CRUD_APP_BASE_PATH } from '../../constants'; -import { getRouterLinkProps, extractQueryParams } from '../../services'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import { extractQueryParams } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterTable } from './remote_cluster_table'; @@ -99,7 +99,7 @@ export class RemoteClusterList extends Component { {isAuthorized && ( @@ -185,7 +185,7 @@ export class RemoteClusterList extends Component { } actions={ { @@ -94,6 +94,7 @@ export class RemoteClusterTable extends Component { render() { const { openDetailPanel } = this.props; const { selectedItems, filteredClusters } = this.state; + const { history } = getRouter(); const columns = [ { @@ -256,7 +257,7 @@ export class RemoteClusterTable extends Component { iconType="pencil" color="primary" isDisabled={isConfiguredByNode} - {...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/edit/${name}`)} + {...reactRouterNavigate(history, `/edit/${name}`)} disabled={isConfiguredByNode} /> diff --git a/x-pack/plugins/remote_clusters/public/application/services/breadcrumb.ts b/x-pack/plugins/remote_clusters/public/application/services/breadcrumb.ts index f90a0d3456166..feec7d523e7c1 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/breadcrumb.ts +++ b/x-pack/plugins/remote_clusters/public/application/services/breadcrumb.ts @@ -6,8 +6,6 @@ import { i18n } from '@kbn/i18n'; -import { CRUD_APP_BASE_PATH } from '../constants'; - interface Breadcrumb { text: string; href?: string; @@ -28,7 +26,7 @@ export function init(setGlobalBreadcrumbs: (breadcrumbs: Breadcrumb[]) => void): text: i18n.translate('xpack.remoteClusters.listBreadcrumbTitle', { defaultMessage: 'Remote Clusters', }), - href: `#${CRUD_APP_BASE_PATH}/list`, + href: `/list`, }, add: { text: i18n.translate('xpack.remoteClusters.addBreadcrumbTitle', { diff --git a/x-pack/plugins/remote_clusters/public/application/services/index.js b/x-pack/plugins/remote_clusters/public/application/services/index.js index 387a04b6e5d8c..ce8d06b6e2278 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/index.js +++ b/x-pack/plugins/remote_clusters/public/application/services/index.js @@ -14,12 +14,6 @@ export { isAddressValid, isPortValid } from './validate_address'; export { extractQueryParams } from './query_params'; -export { - setUserHasLeftApp, - getUserHasLeftApp, - registerRouter, - getRouter, - getRouterLinkProps, -} from './routing'; +export { setUserHasLeftApp, getUserHasLeftApp, registerRouter, getRouter } from './routing'; export { trackUiMetric, METRIC_TYPE } from './ui_metric'; diff --git a/x-pack/plugins/remote_clusters/public/application/services/redirect.ts b/x-pack/plugins/remote_clusters/public/application/services/redirect.ts index 00a97fa74c5ce..1130dbc77fc75 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/redirect.ts +++ b/x-pack/plugins/remote_clusters/public/application/services/redirect.ts @@ -13,5 +13,5 @@ export function init(_navigateToApp: CoreStart['application']['navigateToApp']) } export function redirect(path: string) { - navigateToApp('kibana', { path: `#${path}` }); + navigateToApp('management', { path }); } diff --git a/x-pack/plugins/remote_clusters/public/application/services/routing.js b/x-pack/plugins/remote_clusters/public/application/services/routing.js index 6e60f75fd8bb3..c86c9756cfcc8 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/routing.js +++ b/x-pack/plugins/remote_clusters/public/application/services/routing.js @@ -8,8 +8,6 @@ * This file based on guidance from https://github.com/elastic/eui/blob/master/wiki/react-router.md */ -import { createLocation } from 'history'; - let _userHasLeftApp = false; export function setUserHasLeftApp(userHasLeftApp) { @@ -20,11 +18,6 @@ export function getUserHasLeftApp() { return _userHasLeftApp; } -const isModifiedEvent = (event) => - !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); - -const isLeftClickEvent = (event) => event.button === 0; - let router; export function registerRouter(reactRouter) { router = reactRouter; @@ -33,35 +26,3 @@ export function registerRouter(reactRouter) { export function getRouter() { return router; } - -/** - * The logic for generating hrefs and onClick handlers from the `to` prop is largely borrowed from - * https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js. - */ -export function getRouterLinkProps(to) { - const location = - typeof to === 'string' ? createLocation(to, null, null, router.history.location) : to; - - const href = router.history.createHref(location); - - const onClick = (event) => { - if (event.defaultPrevented) { - return; - } - - // If target prop is set (e.g. to "_blank"), let browser handle link. - if (event.target.getAttribute('target')) { - return; - } - - if (isModifiedEvent(event) || !isLeftClickEvent(event)) { - return; - } - - // Prevent regular link behavior, which causes a browser refresh. - event.preventDefault(); - router.history.push(location); - }; - - return { href, onClick }; -} diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js b/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js index 17523ceda54b5..d57fd37e791a1 100644 --- a/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js +++ b/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; -import { CRUD_APP_BASE_PATH } from '../../constants'; import { addCluster as sendAddClusterRequest, getRouter, @@ -108,7 +107,7 @@ export const addCluster = (cluster) => async (dispatch) => { // This will open the new job in the detail panel. Note that we're *not* showing a success toast // here, because it would partially obscure the detail panel. history.push({ - pathname: `${CRUD_APP_BASE_PATH}/list`, + pathname: `/list`, search: `?cluster=${cluster.name}`, }); } diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js b/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js index 436e6bdce36ed..4fd8faeb7021e 100644 --- a/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js +++ b/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { toasts, fatalError } from '../../services/notification'; -import { CRUD_APP_BASE_PATH } from '../../constants'; import { loadClusters } from './load_clusters'; import { @@ -95,7 +94,7 @@ export const editCluster = (cluster) => async (dispatch) => { // This will open the edited cluster in the detail panel. Note that we're *not* showing a success toast // here, because it would partially obscure the detail panel. history.push({ - pathname: `${CRUD_APP_BASE_PATH}/list`, + pathname: `/list`, search: `?cluster=${cluster.name}`, }); } diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index fde8ffa511319..8881db0f9196e 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -41,7 +41,7 @@ export class RemoteClustersUIPlugin defaultMessage: 'Remote Clusters', }), order: 7, - mount: async ({ element, setBreadcrumbs }) => { + mount: async ({ element, setBreadcrumbs, history }) => { const [core] = await getStartServices(); const { i18n: { Context: i18nContext }, @@ -59,7 +59,7 @@ export class RemoteClustersUIPlugin const isCloudEnabled = Boolean(cloud?.isCloudEnabled); const { renderApp } = await import('./application'); - return renderApp(element, i18nContext, { isCloudEnabled }); + return renderApp(element, i18nContext, { isCloudEnabled }, history); }, }); } diff --git a/x-pack/legacy/plugins/reporting/README.md b/x-pack/plugins/reporting/README.md similarity index 100% rename from x-pack/legacy/plugins/reporting/README.md rename to x-pack/plugins/reporting/README.md diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/common/constants.ts rename to x-pack/plugins/reporting/common/constants.ts diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts b/x-pack/plugins/reporting/common/get_absolute_url.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts rename to x-pack/plugins/reporting/common/get_absolute_url.test.ts diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts b/x-pack/plugins/reporting/common/get_absolute_url.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/common/get_absolute_url.ts rename to x-pack/plugins/reporting/common/get_absolute_url.ts diff --git a/x-pack/plugins/reporting/common/index.ts b/x-pack/plugins/reporting/common/index.ts index 36c896fb4f7b8..cda8934fc8bf6 100644 --- a/x-pack/plugins/reporting/common/index.ts +++ b/x-pack/plugins/reporting/common/index.ts @@ -5,3 +5,4 @@ */ export { CancellationToken } from './cancellation_token'; +export { Poller } from './poller'; diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 5b9ddfb1bbdea..2b9e9299852f5 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -5,7 +5,7 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ConfigType } from '../server/config'; +export { ReportingConfigType } from '../server/config'; export type JobId = string; export type JobStatus = diff --git a/x-pack/legacy/plugins/reporting/common/validate_urls.test.ts b/x-pack/plugins/reporting/common/validate_urls.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/common/validate_urls.test.ts rename to x-pack/plugins/reporting/common/validate_urls.test.ts diff --git a/x-pack/legacy/plugins/reporting/common/validate_urls.ts b/x-pack/plugins/reporting/common/validate_urls.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/common/validate_urls.ts rename to x-pack/plugins/reporting/common/validate_urls.ts diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 9a1d0cec2cf96..772c52dde4a15 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -12,7 +12,7 @@ export const API_BASE_URL = '/api/reporting'; export const API_LIST_URL = `${API_BASE_URL}/jobs`; export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/v1/generate/immediate/csv/saved-object`; -export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/insightsAndAlerting/reporting'; +export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; // Statuses export const JOB_STATUS_FAILED = 'failed'; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index e44bd92c42391..bc1a808d500e0 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -3,18 +3,18 @@ "version": "8.0.0", "kibanaVersion": "kibana", "optionalPlugins": [ + "security", "usageCollection" ], "configPath": ["xpack", "reporting"], "requiredPlugins": [ + "data", "home", "management", "licensing", "uiActions", "embeddable", - "share", - "kibanaLegacy", - "licensing" + "share" ], "server": true, "ui": true diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index c79373665d056..afcae93a8db16 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -266,7 +266,7 @@ class ReportListingUi extends Component { } catch (fetchError) { if (!this.licenseAllowsToShowThisPage()) { this.props.toasts.addDanger(this.state.badLicenseMessage); - this.props.redirect('kibana#/management'); + this.props.redirect('management'); return; } diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 7495e46de47d9..7dd709b956d12 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -26,7 +26,7 @@ import { import { ManagementSectionId, ManagementSetup } from '../../../../src/plugins/management/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../licensing/public'; -import { ConfigType, JobId, JobStatusBuckets } from '../common/types'; +import { ReportingConfigType, JobId, JobStatusBuckets } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; import { getGeneralErrorToast } from './components'; import { ReportListing } from './components/report_listing'; @@ -37,7 +37,7 @@ import { csvReportingProvider } from './share_context_menu/register_csv_reportin import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { - poll: ConfigType['poll']; + poll: ReportingConfigType['poll']; } function getStored(): JobId[] { @@ -111,7 +111,7 @@ export class ReportingPublicPlugin implements Plugin { defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', }), icon: 'reportingApp', - path: '/app/kibana#/management/kibana/reporting', + path: '/app/management/kibana/reporting', showOnHomePage: false, category: FeatureCatalogueCategory.ADMIN, }); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts similarity index 99% rename from x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts rename to x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 6480fb4413f04..898b123e976fd 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -9,7 +9,7 @@ import { map, trunc } from 'lodash'; import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; -import { ViewZoomWidthHeight } from '../../../../export_types/common/layouts/layout'; +import { ViewZoomWidthHeight } from '../../../export_types/common/layouts/layout'; import { LevelLogger } from '../../../lib'; import { ConditionalHeaders, ElementPosition } from '../../../types'; import { allowRequest, NetworkPolicy } from '../../network_policy'; diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/index.ts rename to x-pack/plugins/reporting/server/browsers/chromium/driver/index.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts rename to x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts rename to x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts rename to x-pack/plugins/reporting/server/browsers/chromium/index.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/paths.ts b/x-pack/plugins/reporting/server/browsers/chromium/paths.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/chromium/paths.ts rename to x-pack/plugins/reporting/server/browsers/chromium/paths.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/puppeteer.ts b/x-pack/plugins/reporting/server/browsers/chromium/puppeteer.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/chromium/puppeteer.ts rename to x-pack/plugins/reporting/server/browsers/chromium/puppeteer.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts b/x-pack/plugins/reporting/server/browsers/create_browser_driver_factory.ts similarity index 89% rename from x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts rename to x-pack/plugins/reporting/server/browsers/create_browser_driver_factory.ts index 7b4407890652c..f3486a48ba7b1 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts +++ b/x-pack/plugins/reporting/server/browsers/create_browser_driver_factory.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { first } from 'rxjs/operators'; import { ReportingConfig } from '../'; import { LevelLogger } from '../lib'; import { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; @@ -19,13 +20,13 @@ export async function createBrowserDriverFactory( const browserConfig = captureConfig.browser.chromium; const browserAutoDownload = captureConfig.browser.autoDownload; const browserType = captureConfig.browser.type; - const dataDir = config.kbnConfig.get('path', 'data'); + const dataDir = await config.kbnConfig.get('path', 'data').pipe(first()).toPromise(); if (browserConfig.disableSandbox) { logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`); } if (browserAutoDownload) { - await ensureBrowserDownloaded(browserType); + await ensureBrowserDownloaded(browserType, logger); } try { diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/checksum.ts b/x-pack/plugins/reporting/server/browsers/download/checksum.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/download/checksum.ts rename to x-pack/plugins/reporting/server/browsers/download/checksum.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts b/x-pack/plugins/reporting/server/browsers/download/clean.ts similarity index 85% rename from x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts rename to x-pack/plugins/reporting/server/browsers/download/clean.ts index 8988cbd1c9ec2..8558b001e8174 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts +++ b/x-pack/plugins/reporting/server/browsers/download/clean.ts @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import del from 'del'; import { readdirSync } from 'fs'; import { resolve as resolvePath } from 'path'; - -import del from 'del'; - -import { log, asyncMap } from './util'; +import { LevelLogger } from '../../lib'; +import { asyncMap } from './util'; /** * Delete any file in the `dir` that is not in the expectedPaths */ -export async function clean(dir: string, expectedPaths: string[]) { +export async function clean(dir: string, expectedPaths: string[], logger: LevelLogger) { let filenames: string[]; try { filenames = await readdirSync(dir); @@ -30,7 +29,7 @@ export async function clean(dir: string, expectedPaths: string[]) { await asyncMap(filenames, async (filename) => { const path = resolvePath(dir, filename); if (!expectedPaths.includes(path)) { - log(`Deleting unexpected file ${path}`); + logger.warn(`Deleting unexpected file ${path}`); await del(path, { force: true }); } }); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/download.test.ts b/x-pack/plugins/reporting/server/browsers/download/download.test.ts similarity index 82% rename from x-pack/legacy/plugins/reporting/server/browsers/download/download.test.ts rename to x-pack/plugins/reporting/server/browsers/download/download.test.ts index 05ee2862f017b..b33dfa721d038 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/download.test.ts +++ b/x-pack/plugins/reporting/server/browsers/download/download.test.ts @@ -5,11 +5,11 @@ */ import { createHash } from 'crypto'; -import { resolve as resolvePath } from 'path'; +import del from 'del'; import { readFileSync } from 'fs'; +import { resolve as resolvePath } from 'path'; import { Readable } from 'stream'; - -import del from 'del'; +import { LevelLogger } from '../../lib'; import { download } from './download'; const TEMP_DIR = resolvePath(__dirname, '__tmp__'); @@ -29,6 +29,12 @@ class ReadableOf extends Readable { jest.mock('axios'); const request: jest.Mock = jest.requireMock('axios').request; +const mockLogger = ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), +} as unknown) as LevelLogger; + test('downloads the url to the path', async () => { const BODY = 'abdcefg'; request.mockImplementationOnce(async () => { @@ -37,7 +43,7 @@ test('downloads the url to the path', async () => { }; }); - await download('url', TEMP_FILE); + await download('url', TEMP_FILE, mockLogger); expect(readFileSync(TEMP_FILE, 'utf8')).toEqual(BODY); }); @@ -50,7 +56,7 @@ test('returns the md5 hex hash of the http body', async () => { }; }); - const returned = await download('url', TEMP_FILE); + const returned = await download('url', TEMP_FILE, mockLogger); expect(returned).toEqual(HASH); }); @@ -59,7 +65,7 @@ test('throws if request emits an error', async () => { throw new Error('foo'); }); - return expect(download('url', TEMP_FILE)).rejects.toThrow('foo'); + return expect(download('url', TEMP_FILE, mockLogger)).rejects.toThrow('foo'); }); afterEach(async () => await del(TEMP_DIR)); diff --git a/x-pack/plugins/reporting/server/browsers/download/download.ts b/x-pack/plugins/reporting/server/browsers/download/download.ts new file mode 100644 index 0000000000000..30b50c32a7402 --- /dev/null +++ b/x-pack/plugins/reporting/server/browsers/download/download.ts @@ -0,0 +1,55 @@ +/* + * 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 Axios from 'axios'; +import { createHash } from 'crypto'; +import { closeSync, mkdirSync, openSync, writeSync } from 'fs'; +import { dirname } from 'path'; +import { LevelLogger } from '../../lib'; + +/** + * Download a url and calculate it's checksum + * @param {String} url + * @param {String} path + * @return {Promise} checksum of the downloaded file + */ +export async function download(url: string, path: string, logger: LevelLogger) { + logger.info(`Downloading ${url} to ${path}`); + + const hash = createHash('md5'); + + mkdirSync(dirname(path), { recursive: true }); + const handle = openSync(path, 'w'); + + try { + const resp = await Axios.request({ + url, + method: 'GET', + responseType: 'stream', + }); + + resp.data.on('data', (chunk: Buffer) => { + writeSync(handle, chunk); + hash.update(chunk); + }); + + await new Promise((resolve, reject) => { + resp.data + .on('error', (err: Error) => { + logger.error(err); + reject(err); + }) + .on('end', () => { + logger.info(`Downloaded ${url}`); + resolve(); + }); + }); + } finally { + closeSync(handle); + } + + return hash.digest('hex'); +} diff --git a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts new file mode 100644 index 0000000000000..b334510d71947 --- /dev/null +++ b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts @@ -0,0 +1,79 @@ +/* + * 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 { existsSync } from 'fs'; +import { resolve as resolvePath } from 'path'; +import { BrowserDownload, chromium } from '../'; +import { BROWSER_TYPE } from '../../../common/constants'; +import { LevelLogger } from '../../lib'; +import { md5 } from './checksum'; +import { clean } from './clean'; +import { download } from './download'; +import { asyncMap } from './util'; + +/** + * Check for the downloaded archive of each requested browser type and + * download them if they are missing or their checksum is invalid + * @param {String} browserType + * @return {Promise} + */ +export async function ensureBrowserDownloaded(browserType = BROWSER_TYPE, logger: LevelLogger) { + await ensureDownloaded([chromium], logger); +} + +/** + * Check for the downloaded archive of each requested browser type and + * download them if they are missing or their checksum is invalid* + * @return {Promise} + */ +export async function ensureAllBrowsersDownloaded(logger: LevelLogger) { + await ensureDownloaded([chromium], logger); +} + +/** + * Clears the unexpected files in the browsers archivesPath + * and ensures that all packages/archives are downloaded and + * that their checksums match the declared value + * @param {BrowserSpec} browsers + * @return {Promise} + */ +async function ensureDownloaded(browsers: BrowserDownload[], logger: LevelLogger) { + await asyncMap(browsers, async (browser) => { + const { archivesPath } = browser.paths; + + await clean( + archivesPath, + browser.paths.packages.map((p) => resolvePath(archivesPath, p.archiveFilename)), + logger + ); + + const invalidChecksums: string[] = []; + await asyncMap(browser.paths.packages, async ({ archiveFilename, archiveChecksum }) => { + const url = `${browser.paths.baseUrl}${archiveFilename}`; + const path = resolvePath(archivesPath, archiveFilename); + + if (existsSync(path) && (await md5(path)) === archiveChecksum) { + logger.info(`Browser archive exists in ${path}`); + return; + } + + const downloadedChecksum = await download(url, path, logger); + if (downloadedChecksum !== archiveChecksum) { + invalidChecksums.push(`${url} => ${path}`); + } + }); + + if (invalidChecksums.length) { + const err = new Error( + `Error downloading browsers, checksums incorrect for:\n - ${invalidChecksums.join( + '\n - ' + )}` + ); + logger.error(err); + throw err; + } + }); +} diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/index.ts b/x-pack/plugins/reporting/server/browsers/download/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/download/index.ts rename to x-pack/plugins/reporting/server/browsers/download/index.ts diff --git a/x-pack/plugins/reporting/server/browsers/download/util.ts b/x-pack/plugins/reporting/server/browsers/download/util.ts new file mode 100644 index 0000000000000..99267664be766 --- /dev/null +++ b/x-pack/plugins/reporting/server/browsers/download/util.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Readable } from 'stream'; + +/** + * Iterate an array asynchronously and in parallel + */ +export function asyncMap(array: T[], asyncFn: (x: T) => T2): Promise { + return Promise.all(array.map(asyncFn)); +} + +/** + * Wait for a readable stream to end + */ +export function readableEnd(stream: Readable) { + return new Promise((resolve, reject) => { + stream.on('error', reject).on('end', resolve); + }); +} diff --git a/x-pack/legacy/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md b/x-pack/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md rename to x-pack/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md diff --git a/x-pack/legacy/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md.zip b/x-pack/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md.zip similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md.zip rename to x-pack/plugins/reporting/server/browsers/extract/__tests__/__fixtures__/file.md.zip diff --git a/x-pack/legacy/plugins/reporting/server/browsers/extract/__tests__/extract.js b/x-pack/plugins/reporting/server/browsers/extract/__tests__/extract.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/extract/__tests__/extract.js rename to x-pack/plugins/reporting/server/browsers/extract/__tests__/extract.js diff --git a/x-pack/legacy/plugins/reporting/server/browsers/extract/extract.js b/x-pack/plugins/reporting/server/browsers/extract/extract.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/extract/extract.js rename to x-pack/plugins/reporting/server/browsers/extract/extract.js diff --git a/x-pack/legacy/plugins/reporting/server/browsers/extract/extract_error.js b/x-pack/plugins/reporting/server/browsers/extract/extract_error.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/extract/extract_error.js rename to x-pack/plugins/reporting/server/browsers/extract/extract_error.js diff --git a/x-pack/legacy/plugins/reporting/server/browsers/extract/index.js b/x-pack/plugins/reporting/server/browsers/extract/index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/extract/index.js rename to x-pack/plugins/reporting/server/browsers/extract/index.js diff --git a/x-pack/legacy/plugins/reporting/server/browsers/extract/unzip.js b/x-pack/plugins/reporting/server/browsers/extract/unzip.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/extract/unzip.js rename to x-pack/plugins/reporting/server/browsers/extract/unzip.js diff --git a/x-pack/legacy/plugins/reporting/server/browsers/index.ts b/x-pack/plugins/reporting/server/browsers/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/index.ts rename to x-pack/plugins/reporting/server/browsers/index.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/install.ts b/x-pack/plugins/reporting/server/browsers/install.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/install.ts rename to x-pack/plugins/reporting/server/browsers/install.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/network_policy.test.ts b/x-pack/plugins/reporting/server/browsers/network_policy.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/network_policy.test.ts rename to x-pack/plugins/reporting/server/browsers/network_policy.test.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/network_policy.ts b/x-pack/plugins/reporting/server/browsers/network_policy.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/network_policy.ts rename to x-pack/plugins/reporting/server/browsers/network_policy.ts diff --git a/x-pack/legacy/plugins/reporting/server/browsers/safe_child_process.ts b/x-pack/plugins/reporting/server/browsers/safe_child_process.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/browsers/safe_child_process.ts rename to x-pack/plugins/reporting/server/browsers/safe_child_process.ts diff --git a/x-pack/plugins/reporting/server/config/config.ts b/x-pack/plugins/reporting/server/config/config.ts new file mode 100644 index 0000000000000..4142ab6f0ae43 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/config.ts @@ -0,0 +1,86 @@ +/* + * 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 { Observable } from 'rxjs'; +import { get } from 'lodash'; +import { map } from 'rxjs/operators'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { ReportingConfigType } from './schema'; + +// make config.get() aware of the value type it returns +interface Config { + get(key1: Key1): BaseType[Key1]; + get( + key1: Key1, + key2: Key2 + ): BaseType[Key1][Key2]; + get< + Key1 extends keyof BaseType, + Key2 extends keyof BaseType[Key1], + Key3 extends keyof BaseType[Key1][Key2] + >( + key1: Key1, + key2: Key2, + key3: Key3 + ): BaseType[Key1][Key2][Key3]; + get< + Key1 extends keyof BaseType, + Key2 extends keyof BaseType[Key1], + Key3 extends keyof BaseType[Key1][Key2], + Key4 extends keyof BaseType[Key1][Key2][Key3] + >( + key1: Key1, + key2: Key2, + key3: Key3, + key4: Key4 + ): BaseType[Key1][Key2][Key3][Key4]; +} + +interface KbnServerConfigType { + path: { data: Observable }; + server: { + basePath: string; + host: string; + name: string; + port: number; + protocol: string; + uuid: string; + }; +} + +export interface ReportingConfig extends Config { + kbnConfig: Config; +} + +export const buildConfig = ( + initContext: PluginInitializerContext, + core: CoreSetup, + reportingConfig: ReportingConfigType +): ReportingConfig => { + const { http } = core; + const serverInfo = http.getServerInfo(); + + const kbnConfig = { + path: { + data: initContext.config.legacy.globalConfig$.pipe(map((c) => c.path.data)), + }, + server: { + basePath: core.http.basePath.serverBasePath, + host: serverInfo.host, + name: serverInfo.name, + port: serverInfo.port, + uuid: core.uuid.getInstanceUuid(), + protocol: serverInfo.protocol, + }, + }; + + return { + get: (...keys: string[]) => get(reportingConfig, keys.join('.'), null), // spreading arguments as an array allows the return type to be known by the compiler + kbnConfig: { + get: (...keys: string[]) => get(kbnConfig, keys.join('.'), null), + }, + }; +}; diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index 3107866be6496..1c4c840cfa312 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -5,9 +5,10 @@ */ import * as Rx from 'rxjs'; -import { CoreSetup, Logger, PluginInitializerContext } from 'src/core/server'; -import { ConfigType as ReportingConfigType } from './schema'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { LevelLogger } from '../lib'; import { createConfig$ } from './create_config'; +import { ReportingConfigType } from './schema'; interface KibanaServer { host?: string; @@ -37,14 +38,14 @@ const makeMockCoreSetup = (serverInfo: KibanaServer): CoreSetup => describe('Reporting server createConfig$', () => { let mockCoreSetup: CoreSetup; let mockInitContext: PluginInitializerContext; - let mockLogger: Logger; + let mockLogger: LevelLogger; beforeEach(() => { mockCoreSetup = makeMockCoreSetup({ host: 'kibanaHost', port: 5601, protocol: 'http' }); mockInitContext = makeMockInitContext({ kibanaServer: {}, }); - mockLogger = ({ warn: jest.fn(), debug: jest.fn() } as unknown) as Logger; + mockLogger = ({ warn: jest.fn(), debug: jest.fn() } as unknown) as LevelLogger; }); afterEach(() => { @@ -52,7 +53,8 @@ describe('Reporting server createConfig$', () => { }); it('creates random encryption key and default config using host, protocol, and port from server info', async () => { - const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + const mockConfig$: any = mockInitContext.config.create(); + const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); expect(result.encryptionKey).toMatch(/\S{32,}/); // random 32 characters expect(result.kibanaServer).toMatchInlineSnapshot(` @@ -73,8 +75,8 @@ describe('Reporting server createConfig$', () => { encryptionKey: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii', kibanaServer: {}, }); - const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); - + const mockConfig$: any = mockInitContext.config.create(); + const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); expect(result.encryptionKey).toMatch('iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii'); expect((mockLogger.warn as any).mock.calls.length).toBe(0); }); @@ -88,7 +90,8 @@ describe('Reporting server createConfig$', () => { protocol: 'httpsa', }, }); - const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + const mockConfig$: any = mockInitContext.config.create(); + const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); expect(result).toMatchInlineSnapshot(` Object { @@ -115,7 +118,8 @@ describe('Reporting server createConfig$', () => { encryptionKey: 'aaaaaaaaaaaaabbbbbbbbbbbbaaaaaaaaa', kibanaServer: { hostname: '0' }, }); - const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + const mockConfig$: any = mockInitContext.config.create(); + const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); expect(result.kibanaServer).toMatchInlineSnapshot(` Object { @@ -136,7 +140,8 @@ describe('Reporting server createConfig$', () => { encryptionKey: '888888888888888888888888888888888', capture: { browser: { chromium: { disableSandbox: false } } }, } as ReportingConfigType); - const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + const mockConfig$: any = mockInitContext.config.create(); + const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: false }); expect((mockLogger.warn as any).mock.calls.length).toBe(0); @@ -147,7 +152,8 @@ describe('Reporting server createConfig$', () => { encryptionKey: '888888888888888888888888888888888', capture: { browser: { chromium: { disableSandbox: true } } }, } as ReportingConfigType); - const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + const mockConfig$: any = mockInitContext.config.create(); + const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: true }); expect((mockLogger.warn as any).mock.calls.length).toBe(0); @@ -157,7 +163,8 @@ describe('Reporting server createConfig$', () => { mockInitContext = makeMockInitContext({ encryptionKey: '888888888888888888888888888888888', } as ReportingConfigType); - const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + const mockConfig$: any = mockInitContext.config.create(); + const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: expect.any(Boolean) }); expect((mockLogger.warn as any).mock.calls.length).toBe(0); diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index d363494ddb9a6..67df7dacc77ab 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -5,13 +5,14 @@ */ import { i18n } from '@kbn/i18n/'; -import { TypeOf } from '@kbn/config-schema'; import crypto from 'crypto'; import { capitalize } from 'lodash'; +import { Observable } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; -import { CoreSetup, Logger, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup } from 'src/core/server'; +import { LevelLogger } from '../lib'; import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; -import { ConfigSchema } from './schema'; +import { ReportingConfigType } from './schema'; /* * Set up dynamic config defaults @@ -19,8 +20,12 @@ import { ConfigSchema } from './schema'; * - xpack.kibanaServer * - xpack.reporting.encryptionKey */ -export function createConfig$(core: CoreSetup, context: PluginInitializerContext, logger: Logger) { - return context.config.create>().pipe( +export function createConfig$( + core: CoreSetup, + config$: Observable, + logger: LevelLogger +) { + return config$.pipe( map((config) => { // encryption key let encryptionKey = config.encryptionKey; diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index a0d7618322c65..caa64a7414005 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -5,11 +5,12 @@ */ import { PluginConfigDescriptor } from 'kibana/server'; -import { ConfigSchema, ConfigType } from './schema'; - +import { ConfigSchema, ReportingConfigType } from './schema'; +export { buildConfig } from './config'; export { createConfig$ } from './create_config'; +export { ConfigSchema, ReportingConfigType }; -export const config: PluginConfigDescriptor = { +export const config: PluginConfigDescriptor = { exposeToBrowser: { poll: true }, schema: ConfigSchema, deprecations: ({ unused }) => [ @@ -20,5 +21,3 @@ export const config: PluginConfigDescriptor = { unused('kibanaApp'), ], }; - -export { ConfigSchema, ConfigType }; diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 402fddcb5e014..b1234a6ddf0b6 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -162,6 +162,7 @@ const PollSchema = schema.object({ }); export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), kibanaServer: KibanaServerSchema, queue: QueueSchema, capture: CaptureSchema, @@ -172,4 +173,4 @@ export const ConfigSchema = schema.object({ poll: PollSchema, }); -export type ConfigType = TypeOf; +export type ReportingConfigType = TypeOf; diff --git a/x-pack/legacy/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts similarity index 92% rename from x-pack/legacy/plugins/reporting/server/core.ts rename to x-pack/plugins/reporting/server/core.ts index b89ef9e06b961..e7786b3b753fb 100644 --- a/x-pack/legacy/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -5,22 +5,22 @@ */ import * as Rx from 'rxjs'; -import { first, mapTo, map } from 'rxjs/operators'; +import { first, map, mapTo } from 'rxjs/operators'; import { + BasePath, ElasticsearchServiceSetup, + IRouter, KibanaRequest, + SavedObjectsClientContract, SavedObjectsServiceStart, UiSettingsServiceStart, - IRouter, - SavedObjectsClientContract, - BasePath, } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../plugins/security/server'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; -import { screenshotsObservableFactory } from '../export_types/common/lib/screenshots'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { SecurityPluginSetup } from '../../security/server'; import { ScreenshotsObservableFn } from '../server/types'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; +import { screenshotsObservableFactory } from './export_types/common/lib/screenshots'; import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; import { EnqueueJobFn } from './lib/enqueue_job'; @@ -31,7 +31,7 @@ export interface ReportingInternalSetup { licensing: LicensingPluginSetup; basePath: BasePath['get']; router: IRouter; - security: SecurityPluginSetup; + security?: SecurityPluginSetup; } interface ReportingInternalStart { diff --git a/x-pack/legacy/plugins/reporting/export_types/common/constants.ts b/x-pack/plugins/reporting/server/export_types/common/constants.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/constants.ts rename to x-pack/plugins/reporting/server/export_types/common/constants.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts similarity index 96% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts index fe3ac16b79fe0..4998d936c9b16 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cryptoFactory, LevelLogger } from '../../../server/lib'; +import { cryptoFactory, LevelLogger } from '../../../lib'; import { decryptJobHeaders } from './decrypt_job_headers'; const encryptHeaders = async (encryptionKey: string, headers: Record) => { diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts similarity index 95% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts index a13c1fa2a9efb..e5124c80601d7 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { cryptoFactory, LevelLogger } from '../../../server/lib'; +import { cryptoFactory, LevelLogger } from '../../../lib'; interface HasEncryptedHeaders { headers?: string; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts similarity index 97% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts index 5067d5f5e5dd8..5d651ad5f8aea 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts @@ -5,9 +5,10 @@ */ import sinon from 'sinon'; -import { ReportingConfig, ReportingCore } from '../../../server'; -import { JobDocPayload } from '../../../server/types'; +import { ReportingConfig } from '../../../'; +import { ReportingCore } from '../../../core'; import { createMockReportingCore } from '../../../test_helpers'; +import { JobDocPayload } from '../../../types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './index'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts similarity index 89% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts index 808d5db5c57d5..6854f678aa975 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig } from '../../../server'; -import { ConditionalHeaders } from '../../../server/types'; +import { ReportingConfig } from '../../../'; +import { ConditionalHeaders } from '../../../types'; export const getConditionalHeaders = ({ config, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts similarity index 97% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts index 2cbde69c81316..bd6eb4644d87f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../../../server'; +import { ReportingCore } from '../../../core'; import { createMockReportingCore } from '../../../test_helpers'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './index'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts similarity index 87% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts index 777de317af41e..85d1272fc22ce 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; -import { ReportingConfig, ReportingCore } from '../../../server'; -import { ConditionalHeaders } from '../../../server/types'; +import { ReportingConfig, ReportingCore } from '../../../'; +import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; +import { ConditionalHeaders } from '../../../types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts similarity index 99% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts index 5f55617724ff6..cacea41477ea4 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig } from '../../../server'; +import { ReportingConfig } from '../../../'; import { JobDocPayloadPNG } from '../../png/types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts similarity index 93% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts index 90f3a3b2c9c24..bcd7f122748cb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts @@ -7,12 +7,12 @@ import { format as urlFormat, parse as urlParse, - UrlWithStringQuery, UrlWithParsedQuery, + UrlWithStringQuery, } from 'url'; -import { getAbsoluteUrlFactory } from '../../../common/get_absolute_url'; -import { validateUrls } from '../../../common/validate_urls'; -import { ReportingConfig } from '../../../server'; +import { ReportingConfig } from '../../..'; +import { getAbsoluteUrlFactory } from '../../../../common/get_absolute_url'; +import { validateUrls } from '../../../../common/validate_urls'; import { JobDocPayloadPNG } from '../../png/types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/index.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.test.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts similarity index 95% rename from x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts index 0e5974225b932..5147881a980ea 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts @@ -7,7 +7,7 @@ import { omit } from 'lodash'; import { KBN_SCREENSHOT_HEADER_BLACKLIST, KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, -} from '../../../common/constants'; +} from '../../../../common/constants'; export const omitBlacklistedHeaders = ({ job, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/create_layout.ts b/x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts similarity index 93% rename from x-pack/legacy/plugins/reporting/export_types/common/layouts/create_layout.ts rename to x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts index d33760fcb4f89..216a59d41cec0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CaptureConfig } from '../../../server/types'; +import { CaptureConfig } from '../../../types'; import { LayoutParams, LayoutTypes } from './'; import { Layout } from './layout'; import { PreserveLayout } from './preserve_layout'; diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/index.ts b/x-pack/plugins/reporting/server/export_types/common/layouts/index.ts new file mode 100644 index 0000000000000..23e4c095afe61 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/layouts/index.ts @@ -0,0 +1,66 @@ +/* + * 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 { HeadlessChromiumDriver } from '../../../browsers'; +import { LevelLogger } from '../../../lib'; +import { Layout } from './layout'; + +export { createLayout } from './create_layout'; +export { Layout } from './layout'; +export { PreserveLayout } from './preserve_layout'; +export { PrintLayout } from './print_layout'; + +export const LayoutTypes = { + PRESERVE_LAYOUT: 'preserve_layout', + PRINT: 'print', +}; + +export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ + screenshot: '[data-shared-items-container]', + renderComplete: '[data-shared-item]', + itemsCountAttribute: 'data-shared-items-count', + timefilterDurationAttribute: 'data-shared-timefilter-duration', +}); + +export interface PageSizeParams { + pageMarginTop: number; + pageMarginBottom: number; + pageMarginWidth: number; + tableBorderWidth: number; + headingHeight: number; + subheadingHeight: number; +} + +export interface LayoutSelectorDictionary { + screenshot: string; + renderComplete: string; + itemsCountAttribute: string; + timefilterDurationAttribute: string; +} + +export interface PdfImageSize { + width: number; + height?: number; +} + +export interface Size { + width: number; + height: number; +} + +export interface LayoutParams { + id: string; + dimensions: Size; +} + +interface LayoutSelectors { + // Fields that are not part of Layout: the instances + // independently implement these fields on their own + selectors: LayoutSelectorDictionary; + positionElements?: (browser: HeadlessChromiumDriver, logger: LevelLogger) => Promise; +} + +export type LayoutInstance = Layout & LayoutSelectors & Size; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts b/x-pack/plugins/reporting/server/export_types/common/layouts/layout.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts rename to x-pack/plugins/reporting/server/export_types/common/layouts/layout.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css b/x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.css similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css rename to x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.css diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.ts rename to x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css b/x-pack/plugins/reporting/server/export_types/common/layouts/print.css similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css rename to x-pack/plugins/reporting/server/export_types/common/layouts/print.css diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts b/x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts similarity index 94% rename from x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts rename to x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts index 759f07a33e2b6..30c83771aa3c9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts @@ -6,9 +6,9 @@ import path from 'path'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; -import { LevelLogger } from '../../../server/lib'; -import { HeadlessChromiumDriver } from '../../../server/browsers'; -import { CaptureConfig } from '../../../server/types'; +import { CaptureConfig } from '../../../types'; +import { HeadlessChromiumDriver } from '../../../browsers'; +import { LevelLogger } from '../../../lib'; import { getDefaultLayoutSelectors, LayoutSelectorDictionary, Size, LayoutTypes } from './'; import { Layout } from './layout'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/constants.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/constants.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts similarity index 94% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts index d02e852a3c1b6..140d76f8d1cd6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { AttributesMap, ElementsPositionAndAttribute } from '../../../../server/types'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; +import { AttributesMap, ElementsPositionAndAttribute } from '../../../../types'; import { LayoutInstance } from '../../layouts'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts similarity index 93% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts index 9e446f499ab3a..42eb91ecba830 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { CaptureConfig } from '../../../../server/types'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; +import { CaptureConfig } from '../../../../types'; import { LayoutInstance } from '../../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts similarity index 89% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts index 578a4dd0b975c..05c315b8341a3 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { ElementsPositionAndAttribute, Screenshot } from '../../../../server/types'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; +import { ElementsPositionAndAttribute, Screenshot } from '../../../../types'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts similarity index 90% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts index 74926918584fe..ba68a5fec4e4c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; import { LayoutInstance } from '../../layouts'; import { CONTEXT_GETTIMERANGE } from './constants'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/index.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts similarity index 92% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts index 8a198880a7768..d72afacc1bef3 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; import fs from 'fs'; import { promisify } from 'util'; -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; import { Layout } from '../../layouts/layout'; import { CONTEXT_INJECTCSS } from './constants'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts similarity index 97% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts index cc8b438310430..2ddb4a5d5b994 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../server/browsers/chromium/puppeteer', () => ({ +jest.mock('../../../../browsers/chromium/puppeteer', () => ({ puppeteerLaunch: () => ({ // Fixme needs event emitters newPage: () => ({ @@ -18,14 +18,10 @@ jest.mock('../../../../server/browsers/chromium/puppeteer', () => ({ import * as Rx from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; -import { - CaptureConfig, - ConditionalHeaders, - ElementsPositionAndAttribute, -} from '../../../../server/types'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger } from '../../../../lib'; import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../../../test_helpers'; +import { CaptureConfig, ConditionalHeaders, ElementsPositionAndAttribute } from '../../../../types'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts similarity index 98% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts index bb11d1d3b7b63..028bff4aaa5ee 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts @@ -16,14 +16,14 @@ import { tap, toArray, } from 'rxjs/operators'; -import { HeadlessChromiumDriverFactory } from '../../../../server/browsers'; +import { HeadlessChromiumDriverFactory } from '../../../../browsers'; import { CaptureConfig, ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults, ScreenshotsObservableFn, -} from '../../../../server/types'; +} from '../../../../types'; import { DEFAULT_PAGELOAD_SELECTOR } from '../../constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts similarity index 83% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts index 3cf962b8178fd..bd7e8c508c118 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { CaptureConfig, ConditionalHeaders } from '../../../../server/types'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; +import { CaptureConfig, ConditionalHeaders } from '../../../../types'; export const openUrl = async ( captureConfig: CaptureConfig, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts similarity index 93% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts index c31c55ea8dec6..b6519e914430a 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { CaptureConfig } from '../../../../server/types'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; +import { CaptureConfig } from '../../../../types'; import { LayoutInstance } from '../../layouts'; import { CONTEXT_WAITFORRENDER } from './constants'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts similarity index 91% rename from x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts rename to x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts index ff84d06956dbc..75a7b6516473c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../../../../server/lib'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { CaptureConfig } from '../../../../server/types'; +import { HeadlessChromiumDriver } from '../../../../browsers'; +import { LevelLogger, startTrace } from '../../../../lib'; +import { CaptureConfig } from '../../../../types'; import { LayoutInstance } from '../../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts new file mode 100644 index 0000000000000..8642a6d5758a8 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { + LICENSE_TYPE_BASIC, + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, +} from '../../../common/constants'; +import { CSV_JOB_TYPE as jobType } from '../../../constants'; +import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; +import { metadata } from './metadata'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { JobDocPayloadDiscoverCsv, JobParamsDiscoverCsv } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsDiscoverCsv, + ESQueueCreateJobFn, + JobDocPayloadDiscoverCsv, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentExtension: 'csv', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv/metadata.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/metadata.ts rename to x-pack/plugins/reporting/server/export_types/csv/metadata.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts new file mode 100644 index 0000000000000..acf7f0505a735 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts @@ -0,0 +1,40 @@ +/* + * 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 { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { ReportingCore } from '../../../'; +import { cryptoFactory } from '../../../lib'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../types'; +import { JobParamsDiscoverCsv } from '../types'; + +export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + const setupDeps = reporting.getPluginSetupDeps(); + + return async function createJob( + jobParams: JobParamsDiscoverCsv, + context: RequestHandlerContext, + request: KibanaRequest + ) { + const serializedEncryptedHeaders = await crypto.encrypt(request.headers); + + const savedObjectsClient = context.core.savedObjects.client; + const indexPatternSavedObject = await savedObjectsClient.get( + 'index-pattern', + jobParams.indexPatternId! + ); + + return { + headers: serializedEncryptedHeaders, + indexPatternSavedObject, + basePath: setupDeps.basePath(request), + ...jobParams, + }; + }; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts similarity index 88% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts index dd7b37d989acb..ddcf94079ade4 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts @@ -8,14 +8,18 @@ import nodeCrypto from '@elastic/node-crypto'; // @ts-ignore import Puid from 'puid'; import sinon from 'sinon'; -import { fieldFormats } from '../../../../../../../src/plugins/data/server'; -import { CancellationToken } from '../../../../../../plugins/reporting/common'; -import { CSV_BOM_CHARS } from '../../../common/constants'; -import { LevelLogger } from '../../../server/lib'; -import { setFieldFormats } from '../../../server/services'; +import { fieldFormats, UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; +import { CancellationToken } from '../../../../common'; +import { CSV_BOM_CHARS } from '../../../../common/constants'; +import { LevelLogger } from '../../../lib'; +import { setFieldFormats } from '../../../services'; import { createMockReportingCore } from '../../../test_helpers'; import { JobDocPayloadDiscoverCsv } from '../types'; import { executeJobFactory } from './execute_job'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../../../src/plugins/share/server'; const delay = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(), ms)); @@ -45,7 +49,7 @@ describe('CSV Execute Job', function () { let clusterStub: any; let configGetStub: any; let mockReportingConfig: any; - let mockReportingPlugin: any; + let mockReportingCore: any; let callAsCurrentUserStub: any; let cancellationToken: any; @@ -73,9 +77,10 @@ describe('CSV Execute Job', function () { configGetStub.withArgs('csv', 'scroll').returns({}); mockReportingConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getUiSettingsServiceFactory = () => Promise.resolve(mockUiSettingsClient); - mockReportingPlugin.getElasticsearchService = () => Promise.resolve(mockElasticsearch); + mockReportingCore = await createMockReportingCore(mockReportingConfig); + mockReportingCore.getUiSettingsServiceFactory = () => Promise.resolve(mockUiSettingsClient); + mockReportingCore.getElasticsearchService = () => Promise.resolve(mockElasticsearch); + mockReportingCore.config = mockReportingConfig; cancellationToken = new CancellationToken(); @@ -93,13 +98,13 @@ describe('CSV Execute Job', function () { .stub(clusterStub, 'callAsCurrentUser') .resolves(defaultElasticsearchResponse); - mockUiSettingsClient.get.withArgs('csv:separator').returns(','); - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); + mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(','); + mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); setFieldFormats({ fieldFormatServiceFactory() { const uiConfigMock = {}; - (uiConfigMock as any)['format:defaultTypeMap'] = { + (uiConfigMock as any)[UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = { _default_: { id: 'string', params: {} }, }; @@ -116,7 +121,7 @@ describe('CSV Execute Job', function () { describe('basic Elasticsearch call behavior', function () { it('should decrypt encrypted headers and pass to callAsCurrentUser', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); await executeJob( 'job456', getJobDocPayload({ @@ -136,7 +141,7 @@ describe('CSV Execute Job', function () { testBody: true, }; - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const job = getJobDocPayload({ headers: encryptedHeaders, fields: [], @@ -163,7 +168,7 @@ describe('CSV Execute Job', function () { _scroll_id: scrollId, }); callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); await executeJob( 'job456', getJobDocPayload({ @@ -181,7 +186,7 @@ describe('CSV Execute Job', function () { }); it('should not execute scroll if there are no hits from the search', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); await executeJob( 'job456', getJobDocPayload({ @@ -215,7 +220,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); await executeJob( 'job456', getJobDocPayload({ @@ -254,7 +259,7 @@ describe('CSV Execute Job', function () { _scroll_id: lastScrollId, }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); await executeJob( 'job456', getJobDocPayload({ @@ -286,7 +291,7 @@ describe('CSV Execute Job', function () { _scroll_id: lastScrollId, }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -313,7 +318,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -338,7 +343,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -364,7 +369,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -390,7 +395,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -416,7 +421,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -443,7 +448,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -464,7 +469,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -487,7 +492,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -508,7 +513,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -524,7 +529,7 @@ describe('CSV Execute Job', function () { describe('Elasticsearch call errors', function () { it('should reject Promise if search call errors out', async function () { callAsCurrentUserStub.rejects(new Error()); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: [], @@ -543,7 +548,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); callAsCurrentUserStub.onSecondCall().rejects(new Error()); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: [], @@ -564,7 +569,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: [], @@ -585,7 +590,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: [], @@ -613,7 +618,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: [], @@ -641,7 +646,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: [], @@ -677,7 +682,7 @@ describe('CSV Execute Job', function () { }); it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); executeJob( 'job345', getJobDocPayload({ @@ -696,7 +701,7 @@ describe('CSV Execute Job', function () { }); it(`shouldn't call clearScroll if it never got a scrollId`, async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); executeJob( 'job345', getJobDocPayload({ @@ -714,7 +719,7 @@ describe('CSV Execute Job', function () { }); it('should call clearScroll if it got a scrollId', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); executeJob( 'job345', getJobDocPayload({ @@ -736,7 +741,7 @@ describe('CSV Execute Job', function () { describe('csv content', function () { it('should write column headers to output, even if there are no results', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -747,8 +752,8 @@ describe('CSV Execute Job', function () { }); it('should use custom uiSettings csv:separator for header', async function () { - mockUiSettingsClient.get.withArgs('csv:separator').returns(';'); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(';'); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -759,8 +764,8 @@ describe('CSV Execute Job', function () { }); it('should escape column headers if uiSettings csv:quoteValues is true', async function () { - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -771,8 +776,8 @@ describe('CSV Execute Job', function () { }); it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function () { - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(false); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(false); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -783,7 +788,7 @@ describe('CSV Execute Job', function () { }); it('should write column headers to output, when there are results', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ one: '1', two: '2' }], @@ -803,7 +808,7 @@ describe('CSV Execute Job', function () { }); it('should use comma separated values of non-nested fields from _source', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -824,7 +829,7 @@ describe('CSV Execute Job', function () { }); it('should concatenate the hits from multiple responses', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -852,7 +857,7 @@ describe('CSV Execute Job', function () { }); it('should use field formatters to format fields', async function () { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -894,7 +899,7 @@ describe('CSV Execute Job', function () { beforeEach(async function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -924,7 +929,7 @@ describe('CSV Execute Job', function () { beforeEach(async function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -961,7 +966,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -990,7 +995,7 @@ describe('CSV Execute Job', function () { let maxSizeReached: boolean; beforeEach(async function () { - mockReportingPlugin.getUiSettingsServiceFactory = () => mockUiSettingsClient; + mockReportingCore.getUiSettingsServiceFactory = () => mockUiSettingsClient; configGetStub.withArgs('csv', 'maxSizeBytes').returns(18); callAsCurrentUserStub.onFirstCall().returns({ @@ -1000,7 +1005,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -1037,7 +1042,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -1063,7 +1068,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -1089,7 +1094,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, fields: ['one', 'two'], diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts new file mode 100644 index 0000000000000..4b17cc669efe1 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import Hapi from 'hapi'; +import { IUiSettingsClient, KibanaRequest } from '../../../../../../../src/core/server'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../../../src/plugins/share/server'; +import { ReportingCore } from '../../..'; +import { CSV_BOM_CHARS, CSV_JOB_TYPE } from '../../../../common/constants'; +import { getFieldFormats } from '../../../../server/services'; +import { cryptoFactory, LevelLogger } from '../../../lib'; +import { ESQueueWorkerExecuteFn, ExecuteJobFactory } from '../../../types'; +import { JobDocPayloadDiscoverCsv } from '../types'; +import { fieldFormatMapFactory } from './lib/field_format_map'; +import { createGenerateCsv } from './lib/generate_csv'; + +export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); + const serverBasePath = config.kbnConfig.get('server', 'basePath'); + + return async function executeJob( + jobId: string, + job: JobDocPayloadDiscoverCsv, + cancellationToken: any + ) { + const elasticsearch = await reporting.getElasticsearchService(); + const jobLogger = logger.clone([jobId]); + + const { + searchRequest, + fields, + indexPatternSavedObject, + metaFields, + conflictedTypesFields, + headers, + basePath, + } = job; + + const decryptHeaders = async () => { + try { + if (typeof headers !== 'string') { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage', + { + defaultMessage: 'Job headers are missing', + } + ) + ); + } + return await crypto.decrypt(headers); + } catch (err) { + logger.error(err); + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage', + { + defaultMessage: 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', + values: { encryptionKey: 'xpack.reporting.encryptionKey', err: err.toString() }, + } + ) + ); // prettier-ignore + } + }; + + const fakeRequest = KibanaRequest.from({ + headers: await decryptHeaders(), + // This is used by the spaces SavedObjectClientWrapper to determine the existing space. + // We use the basePath from the saved job, which we'll have post spaces being implemented; + // or we use the server base path, which uses the default space + getBasePath: () => basePath || serverBasePath, + path: '/', + route: { settings: {} }, + url: { href: '/' }, + raw: { req: { url: '/' } }, + } as Hapi.Request); + + const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(fakeRequest); + const callEndpoint = (endpoint: string, clientParams = {}, options = {}) => + callAsCurrentUser(endpoint, clientParams, options); + + const savedObjectsClient = await reporting.getSavedObjectsClient(fakeRequest); + const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); + + const getFormatsMap = async (client: IUiSettingsClient) => { + const fieldFormats = await getFieldFormats().fieldFormatServiceFactory(client); + return fieldFormatMapFactory(indexPatternSavedObject, fieldFormats); + }; + const getUiSettings = async (client: IUiSettingsClient) => { + const [separator, quoteValues, timezone] = await Promise.all([ + client.get(CSV_SEPARATOR_SETTING), + client.get(CSV_QUOTE_VALUES_SETTING), + client.get('dateFormat:tz'), + ]); + + if (timezone === 'Browser') { + logger.warn( + i18n.translate('xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting', { + defaultMessage: 'Kibana Advanced Setting "{dateFormatTimezone}" is set to "Browser". Dates will be formatted as UTC to avoid ambiguity.', + values: { dateFormatTimezone: 'dateFormat:tz' } + }) + ); // prettier-ignore + } + + return { + separator, + quoteValues, + timezone, + }; + }; + + const [formatsMap, uiSettings] = await Promise.all([ + getFormatsMap(uiSettingsClient), + getUiSettings(uiSettingsClient), + ]); + + const generateCsv = createGenerateCsv(jobLogger); + const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; + + const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv({ + searchRequest, + fields, + metaFields, + conflictedTypesFields, + callEndpoint, + cancellationToken, + formatsMap, + settings: { + ...uiSettings, + checkForFormulas: config.get('csv', 'checkForFormulas'), + maxSizeBytes: config.get('csv', 'maxSizeBytes'), + scroll: config.get('csv', 'scroll'), + escapeFormulaValues: config.get('csv', 'escapeFormulaValues'), + }, + }); + + // @TODO: Consolidate these one-off warnings into the warnings array (max-size reached and csv contains formulas) + return { + content_type: 'text/csv', + content: bom + content, + max_size_reached: maxSizeReached, + size, + csv_contains_formulas: csvContainsFormulas, + warnings, + }; + }; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/cell_has_formula.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/cell_has_formula.ts similarity index 85% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/cell_has_formula.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/cell_has_formula.ts index 1433d852ce630..659aef85ed593 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/cell_has_formula.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/lib/cell_has_formula.ts @@ -5,7 +5,7 @@ */ import { startsWith } from 'lodash'; -import { CSV_FORMULA_CHARS } from '../../../../common/constants'; +import { CSV_FORMULA_CHARS } from '../../../../../common/constants'; export const cellHasFormulas = (val: string) => CSV_FORMULA_CHARS.some((formulaChar) => startsWith(val, formulaChar)); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/check_cells_for_formulas.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/check_cells_for_formulas.test.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/check_cells_for_formulas.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/check_cells_for_formulas.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/escape_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/escape_value.test.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/escape_value.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/escape_value.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/field_format_map.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.test.ts similarity index 93% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/field_format_map.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.test.ts index 0434da3d11fe1..83aa23de67663 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/field_format_map.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.test.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { fieldFormats, FieldFormatsGetConfigFn, + UI_SETTINGS, } from '../../../../../../../../src/plugins/data/server'; import { fieldFormatMapFactory } from './field_format_map'; @@ -28,10 +29,10 @@ describe('field format map', function () { }, }; const configMock: Record = {}; - configMock['format:defaultTypeMap'] = { + configMock[UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = { number: { id: 'number', params: {} }, }; - configMock['format:number:defaultPattern'] = '0,0.[000]'; + configMock[UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN] = '0,0.[000]'; const getConfig = ((key: string) => configMock[key]) as FieldFormatsGetConfigFn; const testValue = '4000'; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/field_format_map.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/field_format_map.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/flatten_hit.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/flatten_hit.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.test.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/flatten_hit.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/flatten_hit.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/format_csv_values.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/format_csv_values.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/format_csv_values.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/format_csv_values.test.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/format_csv_values.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/format_csv_values.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/format_csv_values.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/format_csv_values.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/lib/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/generate_csv.ts new file mode 100644 index 0000000000000..019fa3c9c8e9d --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv/server/lib/generate_csv.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { LevelLogger } from '../../../../lib'; +import { GenerateCsvParams, SavedSearchGeneratorResult } from '../../types'; +import { createFlattenHit } from './flatten_hit'; +import { createFormatCsvValues } from './format_csv_values'; +import { createEscapeValue } from './escape_value'; +import { createHitIterator } from './hit_iterator'; +import { MaxSizeStringBuilder } from './max_size_string_builder'; +import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; + +export function createGenerateCsv(logger: LevelLogger) { + const hitIterator = createHitIterator(logger); + + return async function generateCsv({ + searchRequest, + fields, + formatsMap, + metaFields, + conflictedTypesFields, + callEndpoint, + cancellationToken, + settings, + }: GenerateCsvParams): Promise { + const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); + const builder = new MaxSizeStringBuilder(settings.maxSizeBytes); + const header = `${fields.map(escapeValue).join(settings.separator)}\n`; + const warnings: string[] = []; + + if (!builder.tryAppend(header)) { + return { + size: 0, + content: '', + maxSizeReached: true, + warnings: [], + }; + } + + const iterator = hitIterator(settings.scroll, callEndpoint, searchRequest, cancellationToken); + let maxSizeReached = false; + let csvContainsFormulas = false; + + const flattenHit = createFlattenHit(fields, metaFields, conflictedTypesFields); + const formatCsvValues = createFormatCsvValues( + escapeValue, + settings.separator, + fields, + formatsMap + ); + try { + while (true) { + const { done, value: hit } = await iterator.next(); + + if (!hit) { + break; + } + + if (done) { + break; + } + + const flattened = flattenHit(hit); + const rows = formatCsvValues(flattened); + const rowsHaveFormulas = + settings.checkForFormulas && checkIfRowsHaveFormulas(flattened, fields); + + if (rowsHaveFormulas) { + csvContainsFormulas = true; + } + + if (!builder.tryAppend(rows + '\n')) { + logger.warn('max Size Reached'); + maxSizeReached = true; + cancellationToken.cancel(); + break; + } + } + } finally { + await iterator.return(); + } + const size = builder.getSizeInBytes(); + logger.debug(`finished generating, total size in bytes: ${size}`); + + if (csvContainsFormulas && settings.escapeFormulaValues) { + warnings.push( + i18n.translate('xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues', { + defaultMessage: 'CSV may contain formulas whose values have been escaped', + }) + ); + } + + return { + content: builder.getString(), + csvContainsFormulas: csvContainsFormulas && !settings.escapeFormulaValues, + maxSizeReached, + size, + warnings, + }; + }; +} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/hit_iterator.test.ts similarity index 95% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/hit_iterator.test.ts index 7ef4f502b34a3..479879e3c8b01 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/lib/hit_iterator.test.ts @@ -6,9 +6,9 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { CancellationToken } from '../../../../../../../plugins/reporting/common'; -import { LevelLogger } from '../../../../server/lib'; -import { ScrollConfig } from '../../../../server/types'; +import { CancellationToken } from '../../../../../common'; +import { LevelLogger } from '../../../../lib'; +import { ScrollConfig } from '../../../../types'; import { createHitIterator } from './hit_iterator'; const mockLogger = { diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/hit_iterator.ts similarity index 93% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/hit_iterator.ts index 803161910443e..38b28573d602d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/lib/hit_iterator.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { SearchParams, SearchResponse } from 'elasticsearch'; -import { CancellationToken } from '../../../../../../../plugins/reporting/common'; -import { LevelLogger } from '../../../../server/lib'; -import { ScrollConfig } from '../../../../server/types'; +import { CancellationToken } from '../../../../../common'; +import { LevelLogger } from '../../../../lib'; +import { ScrollConfig } from '../../../../types'; async function parseResponse(request: SearchResponse) { const response = await request; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/max_size_string_builder.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/max_size_string_builder.test.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/max_size_string_builder.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/max_size_string_builder.test.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/max_size_string_builder.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/max_size_string_builder.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv/server/lib/max_size_string_builder.ts rename to x-pack/plugins/reporting/server/export_types/csv/server/lib/max_size_string_builder.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts new file mode 100644 index 0000000000000..c80cd5fd24fe5 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -0,0 +1,116 @@ +/* + * 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 { CancellationToken } from '../../../common'; +import { JobParamPostPayload, JobDocPayload, ScrollConfig } from '../../types'; + +export type RawValue = string | object | null | undefined; + +interface DocValueField { + field: string; + format: string; +} + +interface SortOptions { + order: string; + unmapped_type: string; +} + +export interface JobParamPostPayloadDiscoverCsv extends JobParamPostPayload { + state?: { + query: any; + sort: Array>; + docvalue_fields: DocValueField[]; + }; +} + +export interface JobParamsDiscoverCsv { + indexPatternId?: string; + post?: JobParamPostPayloadDiscoverCsv; +} + +export interface JobDocPayloadDiscoverCsv extends JobDocPayload { + basePath: string; + searchRequest: any; + fields: any; + indexPatternSavedObject: any; + metaFields: any; + conflictedTypesFields: any; +} + +export interface SearchRequest { + index: string; + body: + | { + _source: { + excludes: string[]; + includes: string[]; + }; + docvalue_fields: string[]; + query: + | { + bool: { + filter: any[]; + must_not: any[]; + should: any[]; + must: any[]; + }; + } + | any; + script_fields: any; + sort: Array<{ + [key: string]: { + order: string; + }; + }>; + stored_fields: string[]; + } + | any; +} + +type EndpointCaller = (method: string, params: any) => Promise; + +type FormatsMap = Map< + string, + { + id: string; + params: { + pattern: string; + }; + } +>; + +export interface SavedSearchGeneratorResult { + content: string; + size: number; + maxSizeReached: boolean; + csvContainsFormulas?: boolean; + warnings: string[]; +} + +export interface CsvResultFromSearch { + type: string; + result: SavedSearchGeneratorResult; +} + +export interface GenerateCsvParams { + searchRequest: SearchRequest; + callEndpoint: EndpointCaller; + fields: string[]; + formatsMap: FormatsMap; + metaFields: string[]; + conflictedTypesFields: string[]; + cancellationToken: CancellationToken; + settings: { + separator: string; + quoteValues: boolean; + timezone: string | null; + maxSizeBytes: number; + scroll: ScrollConfig; + checkForFormulas?: boolean; + escapeFormulaValues: boolean; + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts new file mode 100644 index 0000000000000..65802ee5bb7fb --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -0,0 +1,48 @@ +/* + * 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 { + LICENSE_TYPE_BASIC, + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, +} from '../../../common/constants'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; +import { ExportTypeDefinition } from '../../types'; +import { metadata } from './metadata'; +import { createJobFactory, ImmediateCreateJobFn } from './server/create_job'; +import { executeJobFactory, ImmediateExecuteFn } from './server/execute_job'; +import { JobParamsPanelCsv } from './types'; + +/* + * These functions are exported to share with the API route handler that + * generates csv from saved object immediately on request. + */ +export { createJobFactory } from './server/create_job'; +export { executeJobFactory } from './server/execute_job'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPanelCsv, + ImmediateCreateJobFn, + JobParamsPanelCsv, + ImmediateExecuteFn +> => ({ + ...metadata, + jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, + jobContentExtension: 'csv', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts similarity index 82% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/metadata.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts index fcef889e52fe4..a0fd8a29fdcc4 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/metadata.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; export const metadata = { id: CSV_FROM_SAVEDOBJECT_JOB_TYPE, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job.ts new file mode 100644 index 0000000000000..c187da5104d3f --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job.ts @@ -0,0 +1,106 @@ +/* + * 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 { notFound, notImplemented } from 'boom'; +import { get } from 'lodash'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { ReportingCore } from '../../../..'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../../common/constants'; +import { cryptoFactory, LevelLogger } from '../../../../lib'; +import { CreateJobFactory, TimeRangeParams } from '../../../../types'; +import { + JobDocPayloadPanelCsv, + JobParamsPanelCsv, + SavedObject, + SavedObjectServiceError, + SavedSearchObjectAttributesJSON, + SearchPanel, + VisObjectAttributesJSON, +} from '../../types'; +import { createJobSearch } from './create_job_search'; + +export type ImmediateCreateJobFn = ( + jobParams: JobParamsType, + headers: KibanaRequest['headers'], + context: RequestHandlerContext, + req: KibanaRequest +) => Promise<{ + type: string | null; + title: string; + jobParams: JobParamsType; +}>; + +interface VisData { + title: string; + visType: string; + panel: SearchPanel; +} + +export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); + + return async function createJob( + jobParams: JobParamsPanelCsv, + headers: KibanaRequest['headers'], + context: RequestHandlerContext, + req: KibanaRequest + ): Promise { + const { savedObjectType, savedObjectId } = jobParams; + const serializedEncryptedHeaders = await crypto.encrypt(headers); + + const { panel, title, visType }: VisData = await Promise.resolve() + .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) + .then(async (savedObject: SavedObject) => { + const { attributes, references } = savedObject; + const { + kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, + } = attributes as SavedSearchObjectAttributesJSON; + const { timerange } = req.body as { timerange: TimeRangeParams }; + + if (!kibanaSavedObjectMetaJSON) { + throw new Error('Could not parse saved object data!'); + } + + const kibanaSavedObjectMeta = { + ...kibanaSavedObjectMetaJSON, + searchSource: JSON.parse(kibanaSavedObjectMetaJSON.searchSourceJSON), + }; + + const { visState: visStateJSON } = attributes as VisObjectAttributesJSON; + if (visStateJSON) { + throw notImplemented('Visualization types are not yet implemented'); + } + + // saved search type + return await createJobSearch(timerange, attributes, references, kibanaSavedObjectMeta); + }) + .catch((err: Error) => { + const boomErr = (err as unknown) as { isBoom: boolean }; + if (boomErr.isBoom) { + throw err; + } + const errPayload: SavedObjectServiceError = get(err, 'output.payload', { statusCode: 0 }); + if (errPayload.statusCode === 404) { + throw notFound(errPayload.message); + } + if (err.stack) { + logger.error(err.stack); + } + throw new Error(`Unable to create a job from saved object data! Error: ${err}`); + }); + + return { + headers: serializedEncryptedHeaders, + jobParams: { ...jobParams, panel, visType }, + type: null, + title, + }; + }; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job_search.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job_search.ts similarity index 95% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job_search.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job_search.ts index 19204ef81c5eb..02abfb90091a1 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job_search.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/create_job_search.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimeRangeParams } from '../../../../server/types'; +import { TimeRangeParams } from '../../../../types'; import { SavedObjectMeta, SavedObjectReference, diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/index.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts new file mode 100644 index 0000000000000..d555100b6320d --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { ReportingCore } from '../../..'; +import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; +import { cryptoFactory, LevelLogger } from '../../../lib'; +import { ExecuteJobFactory, JobDocOutput, JobDocPayload } from '../../../types'; +import { CsvResultFromSearch } from '../../csv/types'; +import { FakeRequest, JobDocPayloadPanelCsv, JobParamsPanelCsv, SearchPanel } from '../types'; +import { createGenerateCsv } from './lib'; + +/* + * ImmediateExecuteFn receives the job doc payload because the payload was + * generated in the CreateFn + */ +export type ImmediateExecuteFn = ( + jobId: null, + job: JobDocPayload, + context: RequestHandlerContext, + req: KibanaRequest +) => Promise; + +export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: LevelLogger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); + const generateCsv = createGenerateCsv(reporting, parentLogger); + + return async function executeJob( + jobId: string | null, + job: JobDocPayloadPanelCsv, + context, + req + ): Promise { + // There will not be a jobID for "immediate" generation. + // jobID is only for "queued" jobs + // Use the jobID as a logging tag or "immediate" + const jobLogger = logger.clone([jobId === null ? 'immediate' : jobId]); + + const { jobParams } = job; + const { isImmediate, panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; + + if (!panel) { + i18n.translate( + 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToAccessPanel', + { defaultMessage: 'Failed to access panel metadata for job execution' } + ); + } + + jobLogger.debug(`Execute job generating [${visType}] csv`); + + let requestObject: KibanaRequest | FakeRequest; + + if (isImmediate && req) { + jobLogger.info(`Executing job from Immediate API using request context`); + requestObject = req; + } else { + jobLogger.info(`Executing job async using encrypted headers`); + let decryptedHeaders: Record; + const serializedEncryptedHeaders = job.headers; + try { + if (typeof serializedEncryptedHeaders !== 'string') { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage', + { + defaultMessage: 'Job headers are missing', + } + ) + ); + } + decryptedHeaders = (await crypto.decrypt(serializedEncryptedHeaders)) as Record< + string, + unknown + >; + } catch (err) { + jobLogger.error(err); + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage', + { + defaultMessage: + 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', + values: { encryptionKey: 'xpack.reporting.encryptionKey', err }, + } + ) + ); + } + + requestObject = { headers: decryptedHeaders }; + } + + let content: string; + let maxSizeReached = false; + let size = 0; + try { + const generateResults: CsvResultFromSearch = await generateCsv( + context, + requestObject, + visType as string, + panel, + jobParams + ); + + ({ + result: { content, maxSizeReached, size }, + } = generateResults); + } catch (err) { + jobLogger.error(`Generate CSV Error! ${err}`); + throw err; + } + + if (maxSizeReached) { + jobLogger.warn(`Max size reached: CSV output truncated to ${size} bytes`); + } + + return { + content_type: CONTENT_TYPE_CSV, + content, + max_size_reached: maxSizeReached, + size, + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv.ts new file mode 100644 index 0000000000000..dd0fb34668e9e --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv.ts @@ -0,0 +1,41 @@ +/* + * 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 { badRequest } from 'boom'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { ReportingCore } from '../../../..'; +import { LevelLogger } from '../../../../lib'; +import { FakeRequest, JobParamsPanelCsv, SearchPanel, VisPanel } from '../../types'; +import { generateCsvSearch } from './generate_csv_search'; + +export function createGenerateCsv(reporting: ReportingCore, logger: LevelLogger) { + return async function generateCsv( + context: RequestHandlerContext, + request: KibanaRequest | FakeRequest, + visType: string, + panel: VisPanel | SearchPanel, + jobParams: JobParamsPanelCsv + ) { + // This should support any vis type that is able to fetch + // and model data on the server-side + + // This structure will not be needed when the vis data just consists of an + // expression that we could run through the interpreter to get csv + switch (visType) { + case 'search': + return await generateCsvSearch( + reporting, + context, + request as KibanaRequest, + panel as SearchPanel, + jobParams, + logger + ); + default: + throw badRequest(`Unsupported or unrecognized saved object type: ${visType}`); + } + }; +} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts similarity index 89% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 848623ede5b2f..3f997a703bef1 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReportingCore } from '../../../../'; import { IUiSettingsClient, KibanaRequest, @@ -15,10 +16,14 @@ import { Filter, IIndexPattern, Query, + UI_SETTINGS, } from '../../../../../../../../src/plugins/data/server'; -import { CancellationToken } from '../../../../../../../plugins/reporting/common'; -import { LevelLogger } from '../../../../server/lib'; -import { ReportingCore } from '../../../../server'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../../../../src/plugins/share/server'; +import { CancellationToken } from '../../../../../common'; +import { LevelLogger } from '../../../../lib'; import { createGenerateCsv } from '../../../csv/server/lib/generate_csv'; import { CsvResultFromSearch, @@ -32,9 +37,9 @@ import { getFilters } from './get_filters'; const getEsQueryConfig = async (config: IUiSettingsClient) => { const configs = await Promise.all([ - config.get('query:allowLeadingWildcards'), - config.get('query:queryString:options'), - config.get('courier:ignoreFilterIfFieldNotInIndex'), + config.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS), + config.get(UI_SETTINGS.QUERY_STRING_OPTIONS), + config.get(UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX), ]); const [allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex] = configs; return { @@ -45,7 +50,10 @@ const getEsQueryConfig = async (config: IUiSettingsClient) => { }; const getUiSettings = async (config: IUiSettingsClient) => { - const configs = await Promise.all([config.get('csv:separator'), config.get('csv:quoteValues')]); + const configs = await Promise.all([ + config.get(CSV_SEPARATOR_SETTING), + config.get(CSV_QUOTE_VALUES_SETTING), + ]); const [separator, quoteValues] = configs; return { separator, quoteValues }; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_data_source.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_data_source.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts similarity index 98% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.test.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts index 110ce91ddfd79..b5d564d93d0d6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimeRangeParams } from '../../../../server/types'; +import { TimeRangeParams } from '../../../../types'; import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../../types'; import { getFilters } from './get_filters'; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts similarity index 96% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts index 4695bbd922581..26631548cc797 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts @@ -6,7 +6,7 @@ import { badRequest } from 'boom'; import moment from 'moment-timezone'; -import { TimeRangeParams } from '../../../../server/types'; +import { TimeRangeParams } from '../../../../types'; import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../../types'; export function getFilters( diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/index.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/index.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts new file mode 100644 index 0000000000000..36ae5b1dac05e --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts @@ -0,0 +1,162 @@ +/* + * 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 { JobParamPostPayload, JobDocPayload, TimeRangeParams } from '../../types'; + +export interface FakeRequest { + headers: Record; +} + +export interface JobParamsPostPayloadPanelCsv extends JobParamPostPayload { + state?: any; +} + +export interface JobParamsPanelCsv { + savedObjectType: string; + savedObjectId: string; + isImmediate: boolean; + panel?: SearchPanel; + post?: JobParamsPostPayloadPanelCsv; + visType?: string; +} + +export interface JobDocPayloadPanelCsv extends JobDocPayload { + jobParams: JobParamsPanelCsv; +} + +export interface SavedObjectServiceError { + statusCode: number; + error?: string; + message?: string; +} + +export interface SavedObjectMetaJSON { + searchSourceJSON: string; +} + +export interface SavedObjectMeta { + searchSource: SearchSource; +} + +export interface SavedSearchObjectAttributesJSON { + title: string; + sort: any[]; + columns: string[]; + kibanaSavedObjectMeta: SavedObjectMetaJSON; + uiState: any; +} + +export interface SavedSearchObjectAttributes { + title: string; + sort: any[]; + columns?: string[]; + kibanaSavedObjectMeta: SavedObjectMeta; + uiState: any; +} + +export interface VisObjectAttributesJSON { + title: string; + visState: string; // JSON string + type: string; + params: any; + uiStateJSON: string; // also JSON string + aggs: any[]; + sort: any[]; + kibanaSavedObjectMeta: SavedObjectMeta; +} + +export interface VisObjectAttributes { + title: string; + visState: string; // JSON string + type: string; + params: any; + uiState: { + vis: { + params: { + sort: { + columnIndex: string; + direction: string; + }; + }; + }; + }; + aggs: any[]; + sort: any[]; + kibanaSavedObjectMeta: SavedObjectMeta; +} + +export interface SavedObjectReference { + name: string; // should be kibanaSavedObjectMeta.searchSourceJSON.index + type: string; // should be index-pattern + id: string; +} + +export interface SavedObject { + attributes: any; + references: SavedObjectReference[]; +} + +/* This object is passed to different helpers in different parts of the code + - packages/kbn-es-query/src/es_query/build_es_query + The structure has redundant parts and json-parsed / json-unparsed versions of the same data + */ +export interface IndexPatternSavedObject { + title: string; + timeFieldName: string; + fields: any[]; + attributes: { + fieldFormatMap: string; + fields: string; + }; +} + +export interface VisPanel { + indexPatternSavedObjectId?: string; + savedSearchObjectId?: string; + attributes: VisObjectAttributes; + timerange: TimeRangeParams; +} + +export interface SearchPanel { + indexPatternSavedObjectId: string; + attributes: SavedSearchObjectAttributes; + timerange: TimeRangeParams; +} + +export interface SearchSourceQuery { + isSearchSourceQuery: boolean; +} + +export interface SearchSource { + query: SearchSourceQuery; + filter: any[]; +} + +/* + * These filter types are stub types to help ensure things get passed to + * non-Typescript functions in the right order. An actual structure is not + * needed because the code doesn't look into the properties; just combines them + * and passes them through to other non-TS modules. + */ +export interface Filter { + isFilter: boolean; +} +export interface TimeFilter extends Filter { + isTimeFilter: boolean; +} +export interface QueryFilter extends Filter { + isQueryFilter: boolean; +} +export interface SearchSourceFilter extends Filter { + isSearchSourceFilter: boolean; +} + +export interface IndexPatternField { + scripted: boolean; + lang?: string; + script?: string; + name: string; +} diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts new file mode 100644 index 0000000000000..a3b51e365e772 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -0,0 +1,40 @@ +/* + * 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 { + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, + PNG_JOB_TYPE as jobType, +} from '../../../common/constants'; +import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../..//types'; +import { metadata } from './metadata'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { JobDocPayloadPNG, JobParamsPNG } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPNG, + ESQueueCreateJobFn, + JobDocPayloadPNG, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'PNG', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/metadata.ts b/x-pack/plugins/reporting/server/export_types/png/metadata.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/png/metadata.ts rename to x-pack/plugins/reporting/server/export_types/png/metadata.ts diff --git a/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts new file mode 100644 index 0000000000000..3f1556fb29782 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validateUrls } from '../../../../../common/validate_urls'; +import { cryptoFactory } from '../../../../lib'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../types'; +import { JobParamsPNG } from '../../types'; + +export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting) { + const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); + const crypto = cryptoFactory(config.get('encryptionKey')); + + return async function createJob( + { objectType, title, relativeUrl, browserTimezone, layout }, + context, + req + ) { + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); + + validateUrls([relativeUrl]); + + return { + objectType, + title, + relativeUrl, + headers: serializedEncryptedHeaders, + browserTimezone, + layout, + basePath: setupDeps.basePath(req), + forceNow: new Date().toISOString(), + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.test.ts new file mode 100644 index 0000000000000..b92f53ff6563f --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.test.ts @@ -0,0 +1,156 @@ +/* + * 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 * as Rx from 'rxjs'; +import { ReportingCore } from '../../../../'; +import { CancellationToken } from '../../../../../common'; +import { cryptoFactory, LevelLogger } from '../../../../lib'; +import { createMockReportingCore } from '../../../../test_helpers'; +import { JobDocPayloadPNG } from '../../types'; +import { generatePngObservableFactory } from '../lib/generate_png'; +import { executeJobFactory } from './'; + +jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); + +let mockReporting: ReportingCore; + +const cancellationToken = ({ + on: jest.fn(), +} as unknown) as CancellationToken; + +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); + +const mockEncryptionKey = 'abcabcsecuresecret'; +const encryptHeaders = async (headers: Record) => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; + +const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPNG; + +beforeEach(async () => { + const kbnConfig = { + 'server.basePath': '/sbp', + }; + const reportingConfig = { + index: '.reporting-2018.10.10', + encryptionKey: mockEncryptionKey, + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + 'queue.indexInterval': 'daily', + 'queue.timeout': Infinity, + }; + const mockReportingConfig = { + get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], + kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, + }; + + mockReporting = await createMockReportingCore(mockReportingConfig); + + const mockElasticsearch = { + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, + }, + }; + const mockGetElasticsearch = jest.fn(); + mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); + mockReporting.getElasticsearchService = mockGetElasticsearch; + // @ts-ignore over-riding config method + mockReporting.config = mockReportingConfig; + + (generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn()); +}); + +afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); + +test(`passes browserTimezone to generatePng`, async () => { + const encryptedHeaders = await encryptHeaders({}); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const browserTimezone = 'UTC'; + await executeJob( + 'pngJobId', + getJobDocPayload({ + relativeUrl: '/app/kibana#/something', + browserTimezone, + headers: encryptedHeaders, + }), + cancellationToken + ); + + expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + LevelLogger { + "_logger": Object { + "get": [MockFunction], + }, + "_tags": Array [ + "PNG", + "execute", + "pngJobId", + ], + "warning": [Function], + }, + "http://localhost:5601/sbp/app/kibana#/something", + "UTC", + Object { + "conditions": Object { + "basePath": "/sbp", + "hostname": "localhost", + "port": 5601, + "protocol": "http", + }, + "headers": Object {}, + }, + undefined, + ], + ] + `); +}); + +test(`returns content_type of application/png`, async () => { + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + + const generatePngObservable = await generatePngObservableFactory(mockReporting); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of('foo')); + + const { content_type: contentType } = await executeJob( + 'pngJobId', + getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + cancellationToken + ); + expect(contentType).toBe('image/png'); +}); + +test(`returns content of generatePng getBuffer base64 encoded`, async () => { + const testContent = 'raw string from get_screenhots'; + const generatePngObservable = await generatePngObservableFactory(mockReporting); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ base64: testContent })); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + const { content } = await executeJob( + 'pngJobId', + getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + cancellationToken + ); + + expect(content).toEqual(testContent); +}); diff --git a/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.ts new file mode 100644 index 0000000000000..ea4c4b1d106ae --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png/server/execute_job/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import apm from 'elastic-apm-node'; +import * as Rx from 'rxjs'; +import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; +import { ReportingCore } from '../../../..'; +import { PNG_JOB_TYPE } from '../../../../../common/constants'; +import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput } from '../../../..//types'; +import { LevelLogger } from '../../../../lib'; +import { + decryptJobHeaders, + getConditionalHeaders, + getFullUrls, + omitBlacklistedHeaders, +} from '../../../common/execute_job/'; +import { JobDocPayloadPNG } from '../../types'; +import { generatePngObservableFactory } from '../lib/generate_png'; + +type QueuedPngExecutorFactory = ExecuteJobFactory>; + +export const executeJobFactory: QueuedPngExecutorFactory = async function executeJobFactoryFn( + reporting: ReportingCore, + parentLogger: LevelLogger +) { + const config = reporting.getConfig(); + const encryptionKey = config.get('encryptionKey'); + const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); + + return async function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { + const apmTrans = apm.startTransaction('reporting execute_job png', 'reporting'); + const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); + let apmGeneratePng: { end: () => void } | null | undefined; + + const generatePngObservable = await generatePngObservableFactory(reporting); + const jobLogger = logger.clone([jobId]); + const process$: Rx.Observable = Rx.of(1).pipe( + mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), + map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), + map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), + mergeMap((conditionalHeaders) => { + const urls = getFullUrls({ config, job }); + const hashUrl = urls[0]; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePng = apmTrans?.startSpan('generate_png_pipeline', 'execute'); + return generatePngObservable( + jobLogger, + hashUrl, + job.browserTimezone, + conditionalHeaders, + job.layout + ); + }), + map(({ base64, warnings }) => { + if (apmGeneratePng) apmGeneratePng.end(); + + return { + content_type: 'image/png', + content: base64, + size: (base64 && base64.length) || 0, + + warnings, + }; + }), + catchError((err) => { + jobLogger.error(err); + return Rx.throwError(err); + }) + ); + + const stop$ = Rx.fromEventPattern(cancellationToken.on); + return process$.pipe(takeUntil(stop$)).toPromise(); + }; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts similarity index 94% rename from x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts rename to x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts index 0ad381885f4c4..d7e9d0f812b37 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts @@ -7,9 +7,9 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; -import { ReportingCore } from '../../../../server'; -import { LevelLogger } from '../../../../server/lib'; -import { ConditionalHeaders, ScreenshotResults } from '../../../../server/types'; +import { ReportingCore } from '../../../../'; +import { LevelLogger } from '../../../../lib'; +import { ConditionalHeaders, ScreenshotResults } from '../../../../types'; import { LayoutParams } from '../../../common/layouts'; import { PreserveLayout } from '../../../common/layouts/preserve_layout'; diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts new file mode 100644 index 0000000000000..486a8e91a722f --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { JobDocPayload } from '../../../server/types'; +import { LayoutInstance, LayoutParams } from '../common/layouts'; + +// Job params: structure of incoming user request data +export interface JobParamsPNG { + objectType: string; + title: string; + relativeUrl: string; + browserTimezone: string; + layout: LayoutInstance; +} + +// Job payload: structure of stored job data provided by create_job +export interface JobDocPayloadPNG extends JobDocPayload { + basePath?: string; + browserTimezone: string; + forceNow?: string; + layout: LayoutParams; + relativeUrl: string; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts new file mode 100644 index 0000000000000..39a0cbd5270a1 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -0,0 +1,40 @@ +/* + * 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 { + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, + PDF_JOB_TYPE as jobType, +} from '../../../common/constants'; +import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; +import { metadata } from './metadata'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { JobDocPayloadPDF, JobParamsPDF } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPDF, + ESQueueCreateJobFn, + JobDocPayloadPDF, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/metadata.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/metadata.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/metadata.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/metadata.ts diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts new file mode 100644 index 0000000000000..06a0902a56954 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validateUrls } from '../../../../../common/validate_urls'; +import { cryptoFactory } from '../../../../lib'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../types'; +import { JobParamsPDF } from '../../types'; + +export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting) { + const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); + const crypto = cryptoFactory(config.get('encryptionKey')); + + return async function createJobFn( + { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, + context, + req + ) { + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); + + validateUrls(relativeUrls); + + return { + basePath: setupDeps.basePath(req), + browserTimezone, + forceNow: new Date().toISOString(), + headers: serializedEncryptedHeaders, + layout, + relativeUrls, + title, + objectType, + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.test.ts new file mode 100644 index 0000000000000..b8e1e5eebd9e7 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.test.ts @@ -0,0 +1,159 @@ +/* + * 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. + */ + +jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); + +import * as Rx from 'rxjs'; +import { ReportingCore } from '../../../../'; +import { CancellationToken } from '../../../../../common'; +import { cryptoFactory, LevelLogger } from '../../../../lib'; +import { createMockReportingCore } from '../../../../test_helpers'; +import { JobDocPayloadPDF } from '../../types'; +import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { executeJobFactory } from './'; + +let mockReporting: ReportingCore; + +const cancellationToken = ({ + on: jest.fn(), +} as unknown) as CancellationToken; + +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); + +const mockEncryptionKey = 'testencryptionkey'; +const encryptHeaders = async (headers: Record) => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; + +const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPDF; + +beforeEach(async () => { + const kbnConfig = { + 'server.basePath': '/sbp', + }; + const reportingConfig = { + index: '.reports-test', + encryptionKey: mockEncryptionKey, + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + }; + const mockReportingConfig = { + get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], + kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, + }; + + mockReporting = await createMockReportingCore(mockReportingConfig); + + const mockElasticsearch = { + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, + }, + }; + const mockGetElasticsearch = jest.fn(); + mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); + mockReporting.getElasticsearchService = mockGetElasticsearch; + // @ts-ignore over-riding config + mockReporting.config = mockReportingConfig; + + (generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn()); +}); + +afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); + +test(`passes browserTimezone to generatePdf`, async () => { + const encryptedHeaders = await encryptHeaders({}); + const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; + generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const browserTimezone = 'UTC'; + await executeJob( + 'pdfJobId', + getJobDocPayload({ + relativeUrl: '/app/kibana#/something', + browserTimezone, + headers: encryptedHeaders, + }), + cancellationToken + ); + + expect(generatePdfObservable.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + LevelLogger { + "_logger": Object { + "get": [MockFunction], + }, + "_tags": Array [ + "printable_pdf", + "execute", + "pdfJobId", + ], + "warning": [Function], + }, + undefined, + Array [ + "http://localhost:5601/sbp/app/kibana#/something", + ], + "UTC", + Object { + "conditions": Object { + "basePath": "/sbp", + "hostname": "localhost", + "port": 5601, + "protocol": "http", + }, + "headers": Object {}, + }, + undefined, + false, + ], + ] + `); +}); + +test(`returns content_type of application/pdf`, async () => { + const logger = getMockLogger(); + const executeJob = await executeJobFactory(mockReporting, logger); + const encryptedHeaders = await encryptHeaders({}); + + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + + const { content_type: contentType } = await executeJob( + 'pdfJobId', + getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), + cancellationToken + ); + expect(contentType).toBe('application/pdf'); +}); + +test(`returns content of generatePdf getBuffer base64 encoded`, async () => { + const testContent = 'test content'; + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + const { content } = await executeJob( + 'pdfJobId', + getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), + cancellationToken + ); + + expect(content).toEqual(Buffer.from(testContent).toString('base64')); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts new file mode 100644 index 0000000000000..a4d84b2f9f1e0 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts @@ -0,0 +1,92 @@ +/* + * 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 apm from 'elastic-apm-node'; +import * as Rx from 'rxjs'; +import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; +import { ReportingCore } from '../../../..'; +import { PDF_JOB_TYPE } from '../../../../../common/constants'; +import { LevelLogger } from '../../../../lib'; +import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput } from '../../../../types'; +import { + decryptJobHeaders, + getConditionalHeaders, + getCustomLogo, + getFullUrls, + omitBlacklistedHeaders, +} from '../../../common/execute_job'; +import { JobDocPayloadPDF } from '../../types'; +import { generatePdfObservableFactory } from '../lib/generate_pdf'; + +type QueuedPdfExecutorFactory = ExecuteJobFactory>; + +export const executeJobFactory: QueuedPdfExecutorFactory = async function executeJobFactoryFn( + reporting: ReportingCore, + parentLogger: LevelLogger +) { + const config = reporting.getConfig(); + const encryptionKey = config.get('encryptionKey'); + + const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']); + + return async function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { + const apmTrans = apm.startTransaction('reporting execute_job pdf', 'reporting'); + const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); + let apmGeneratePdf: { end: () => void } | null | undefined; + + const generatePdfObservable = await generatePdfObservableFactory(reporting); + + const jobLogger = logger.clone([jobId]); + const process$: Rx.Observable = Rx.of(1).pipe( + mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), + map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), + map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), + mergeMap((conditionalHeaders) => + getCustomLogo({ reporting, config, job, conditionalHeaders }) + ), + mergeMap(({ logo, conditionalHeaders }) => { + const urls = getFullUrls({ config, job }); + + const { browserTimezone, layout, title } = job; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute'); + return generatePdfObservable( + jobLogger, + title, + urls, + browserTimezone, + conditionalHeaders, + layout, + logo + ); + }), + map(({ buffer, warnings }) => { + if (apmGeneratePdf) apmGeneratePdf.end(); + + const apmEncode = apmTrans?.startSpan('encode_pdf', 'output'); + const content = buffer?.toString('base64') || null; + if (apmEncode) apmEncode.end(); + + return { + content_type: 'application/pdf', + content, + size: buffer?.byteLength || 0, + warnings, + }; + }), + catchError((err) => { + jobLogger.error(err); + return Rx.throwError(err); + }) + ); + + const stop$ = Rx.fromEventPattern(cancellationToken.on); + + if (apmTrans) apmTrans.end(); + return process$.pipe(takeUntil(stop$)).toPromise(); + }; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts similarity index 96% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts index 3b626c8f0da44..366949a033757 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -7,9 +7,9 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -import { ReportingCore } from '../../../../server'; -import { LevelLogger } from '../../../../server/lib'; -import { ConditionalHeaders, ScreenshotResults } from '../../../../server/types'; +import { ReportingCore } from '../../../../'; +import { LevelLogger } from '../../../../lib'; +import { ConditionalHeaders, ScreenshotResults } from '../../../../types'; import { createLayout, LayoutInstance, LayoutParams } from '../../../common/layouts'; // @ts-ignore untyped module import { pdf } from './pdf'; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/index.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/index.js diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/tracker.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/tracker.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/tracker.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js similarity index 100% rename from x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts new file mode 100644 index 0000000000000..087ef5a6ca82c --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { JobDocPayload } from '../../../server/types'; +import { LayoutInstance, LayoutParams } from '../common/layouts'; + +// Job params: structure of incoming user request data, after being parsed from RISON +export interface JobParamsPDF { + objectType: string; // visualization, dashboard, etc. Used for job info & telemetry + title: string; + relativeUrls: string[]; + browserTimezone: string; + layout: LayoutInstance; +} + +// Job payload: structure of stored job data provided by create_job +export interface JobDocPayloadPDF extends JobDocPayload { + basePath?: string; + browserTimezone: string; + forceNow?: string; + layout: LayoutParams; + relativeUrls: string[]; +} diff --git a/x-pack/plugins/reporting/server/index.ts b/x-pack/plugins/reporting/server/index.ts index 9d34eba70d0f4..c78e042d019e8 100644 --- a/x-pack/plugins/reporting/server/index.ts +++ b/x-pack/plugins/reporting/server/index.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from 'kibana/server'; import { ReportingPlugin } from './plugin'; +import { ReportingConfigType } from './config'; -export { config, ConfigSchema, ConfigType } from './config'; -export { PluginsSetup } from './plugin'; +export const plugin = (initContext: PluginInitializerContext) => + new ReportingPlugin(initContext); -export const plugin = (initializerContext: PluginInitializerContext) => - new ReportingPlugin(initializerContext); +export { ReportingPlugin as Plugin }; +export { config } from './config'; +export { ReportingSetupDeps as PluginSetup } from './types'; +export { ReportingStartDeps as PluginStart } from './types'; + +// internal imports +export { ReportingCore } from './core'; +export { ReportingConfig } from './config/config'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/__tests__/export_types_registry.js b/x-pack/plugins/reporting/server/lib/__tests__/export_types_registry.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/__tests__/export_types_registry.js rename to x-pack/plugins/reporting/server/lib/__tests__/export_types_registry.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts b/x-pack/plugins/reporting/server/lib/check_license.test.ts similarity index 99% rename from x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts rename to x-pack/plugins/reporting/server/lib/check_license.test.ts index 366a8d94286f1..aa9f86533ef61 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts +++ b/x-pack/plugins/reporting/server/lib/check_license.test.ts @@ -5,7 +5,7 @@ */ import { checkLicense } from './check_license'; -import { ILicense } from '../../../../../plugins/licensing/server'; +import { ILicense } from '../../../licensing/server'; import { ExportTypesRegistry } from './export_types_registry'; describe('check_license', () => { diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts b/x-pack/plugins/reporting/server/lib/check_license.ts similarity index 97% rename from x-pack/legacy/plugins/reporting/server/lib/check_license.ts rename to x-pack/plugins/reporting/server/lib/check_license.ts index 1b4eeaa0bae3e..a764aa1f1eec6 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts +++ b/x-pack/plugins/reporting/server/lib/check_license.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILicense } from '../../../../../plugins/licensing/server'; +import { ILicense } from '../../../licensing/server'; import { ExportTypeDefinition } from '../types'; import { ExportTypesRegistry } from './export_types_registry'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/plugins/reporting/server/lib/create_queue.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/create_queue.ts rename to x-pack/plugins/reporting/server/lib/create_queue.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_tagged_logger.ts b/x-pack/plugins/reporting/server/lib/create_tagged_logger.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/create_tagged_logger.ts rename to x-pack/plugins/reporting/server/lib/create_tagged_logger.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/plugins/reporting/server/lib/create_worker.test.ts similarity index 96% rename from x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts rename to x-pack/plugins/reporting/server/lib/create_worker.test.ts index 1193091075e3e..8e1174e01aa7f 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.test.ts @@ -6,7 +6,7 @@ import * as sinon from 'sinon'; import { ReportingConfig, ReportingCore } from '../../server'; -import { createMockReportingCore } from '../../test_helpers'; +import { createMockReportingCore } from '../test_helpers'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; @@ -42,6 +42,8 @@ describe('Create Worker', () => { mockConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; mockReporting = await createMockReportingCore(mockConfig); mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry(); + // @ts-ignore over-riding config manually + mockReporting.config = mockConfig; client = new ClientMock(); queue = new Esqueue('reporting-queue', { client }); executeJobFactoryStub.reset(); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts similarity index 97% rename from x-pack/legacy/plugins/reporting/server/lib/create_worker.ts rename to x-pack/plugins/reporting/server/lib/create_worker.ts index 57bd61aee7195..c9e865668bb30 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CancellationToken } from '../../../../../plugins/reporting/common'; +import { CancellationToken } from '../../common'; import { PLUGIN_ID } from '../../common/constants'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/crypto.ts b/x-pack/plugins/reporting/server/lib/crypto.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/crypto.ts rename to x-pack/plugins/reporting/server/lib/crypto.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts similarity index 97% rename from x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts rename to x-pack/plugins/reporting/server/lib/enqueue_job.ts index 6367c8a1da98a..3837f593df5b2 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -6,7 +6,7 @@ import { EventEmitter } from 'events'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { AuthenticatedUser } from '../../../../../plugins/security/server'; +import { AuthenticatedUser } from '../../../security/server'; import { ESQueueCreateJobFn } from '../../server/types'; import { ReportingCore } from '../core'; // @ts-ignore diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/job.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/job.js new file mode 100644 index 0000000000000..9cc62800d439f --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/job.js @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import events from 'events'; + +export class JobMock extends events.EventEmitter { + constructor(queue, index, type, payload, options = {}) { + super(); + + this.queue = queue; + this.index = index; + this.jobType = type; + this.payload = payload; + this.options = options; + } + + getProp(name) { + return this[name]; + } +} diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js rename to x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/queue.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/queue.js new file mode 100644 index 0000000000000..974cb4a5e2a6e --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/queue.js @@ -0,0 +1,17 @@ +/* + * 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 events from 'events'; + +export class QueueMock extends events.EventEmitter { + constructor() { + super(); + } + + setClient(client) { + this.client = client; + } +} diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/worker.js new file mode 100644 index 0000000000000..fe8a859ccb445 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/worker.js @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import events from 'events'; + +export class WorkerMock extends events.EventEmitter { + constructor(queue, type, workerFn, opts = {}) { + super(); + + this.queue = queue; + this.type = type; + this.workerFn = workerFn; + this.options = opts; + } + + getProp(name) { + return this[name]; + } +} diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js rename to x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/errors.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/errors.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/errors.js rename to x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/errors.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/index_timestamp.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/index_timestamp.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/index_timestamp.js rename to x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/index_timestamp.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js rename to x-pack/plugins/reporting/server/lib/esqueue/__tests__/index.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js rename to x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js rename to x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/default_settings.js b/x-pack/plugins/reporting/server/lib/esqueue/constants/default_settings.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/default_settings.js rename to x-pack/plugins/reporting/server/lib/esqueue/constants/default_settings.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/events.js b/x-pack/plugins/reporting/server/lib/esqueue/constants/events.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/events.js rename to x-pack/plugins/reporting/server/lib/esqueue/constants/events.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/index.js b/x-pack/plugins/reporting/server/lib/esqueue/constants/index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/index.js rename to x-pack/plugins/reporting/server/lib/esqueue/constants/index.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.ts b/x-pack/plugins/reporting/server/lib/esqueue/constants/statuses.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.ts rename to x-pack/plugins/reporting/server/lib/esqueue/constants/statuses.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js b/x-pack/plugins/reporting/server/lib/esqueue/helpers/create_index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js rename to x-pack/plugins/reporting/server/lib/esqueue/helpers/create_index.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/errors.js b/x-pack/plugins/reporting/server/lib/esqueue/helpers/errors.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/errors.js rename to x-pack/plugins/reporting/server/lib/esqueue/helpers/errors.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js b/x-pack/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js rename to x-pack/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js b/x-pack/plugins/reporting/server/lib/esqueue/index.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js rename to x-pack/plugins/reporting/server/lib/esqueue/index.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js b/x-pack/plugins/reporting/server/lib/esqueue/job.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js rename to x-pack/plugins/reporting/server/lib/esqueue/job.js diff --git a/x-pack/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/worker.js new file mode 100644 index 0000000000000..b26ed731c6831 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/esqueue/worker.js @@ -0,0 +1,463 @@ +/* + * 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 events from 'events'; +import moment from 'moment'; +import Puid from 'puid'; +import { CancellationToken, Poller } from '../../../common'; +import { constants } from './constants'; +import { UnspecifiedWorkerError, WorkerTimeoutError } from './helpers/errors'; + +const puid = new Puid(); + +export function formatJobObject(job) { + return { + index: job._index, + id: job._id, + }; +} + +export function getUpdatedDocPath(response) { + const { _index: ind, _id: id } = response; + return `/${ind}/${id}`; +} + +const MAX_PARTIAL_ERROR_LENGTH = 1000; // 1000 of beginning, 1000 of end +const ERROR_PARTIAL_SEPARATOR = '...'; +const MAX_ERROR_LENGTH = MAX_PARTIAL_ERROR_LENGTH * 2 + ERROR_PARTIAL_SEPARATOR.length; + +function getLogger(opts, id, logLevel) { + return (msg, err) => { + /* + * This does not get the logger instance from queue.registerWorker in the createWorker function. + * The logger instance in the Equeue lib comes from createTaggedLogger, so logLevel tags are passed differently + */ + const logger = opts.logger || function () {}; + const message = `${id} - ${msg}`; + const tags = [logLevel]; + + if (err) { + // The error message string could be very long if it contains the request + // body of a request that was too large for Elasticsearch. + // This takes a partial version of the error message without scanning + // every character of the string, which would block Node. + const errString = `${message}: ${err.stack ? err.stack : err}`; + const errLength = errString.length; + const subStr = String.prototype.substring.bind(errString); + if (errLength > MAX_ERROR_LENGTH) { + const partialError = + subStr(0, MAX_PARTIAL_ERROR_LENGTH) + + ERROR_PARTIAL_SEPARATOR + + subStr(errLength - MAX_PARTIAL_ERROR_LENGTH); + + logger(partialError, tags); + logger( + `A partial version of the entire error message was logged. ` + + `The entire error message length is: ${errLength} characters.`, + tags + ); + } else { + logger(errString, tags); + } + return; + } + + logger(message, tags); + }; +} + +export class Worker extends events.EventEmitter { + constructor(queue, type, workerFn, opts) { + if (typeof type !== 'string') throw new Error('type must be a string'); + if (typeof workerFn !== 'function') throw new Error('workerFn must be a function'); + if (typeof opts !== 'object') throw new Error('opts must be an object'); + if (typeof opts.interval !== 'number') throw new Error('opts.interval must be a number'); + if (typeof opts.intervalErrorMultiplier !== 'number') + throw new Error('opts.intervalErrorMultiplier must be a number'); + + super(); + + this.id = puid.generate(); + this.kibanaId = opts.kibanaId; + this.kibanaName = opts.kibanaName; + this.queue = queue; + this._client = this.queue.client; + this.jobtype = type; + this.workerFn = workerFn; + + this.debug = getLogger(opts, this.id, 'debug'); + this.warn = getLogger(opts, this.id, 'warning'); + this.error = getLogger(opts, this.id, 'error'); + this.info = getLogger(opts, this.id, 'info'); + + this._running = true; + this.debug(`Created worker for ${this.jobtype} jobs`); + + this._poller = new Poller({ + functionToPoll: () => { + return this._processPendingJobs(); + }, + pollFrequencyInMillis: opts.interval, + trailing: true, + continuePollingOnError: true, + pollFrequencyErrorMultiplier: opts.intervalErrorMultiplier, + }); + this._startJobPolling(); + } + + destroy() { + this._running = false; + this._stopJobPolling(); + } + + toJSON() { + return { + id: this.id, + index: this.queue.index, + jobType: this.jobType, + }; + } + + emit(name, ...args) { + super.emit(name, ...args); + this.queue.emit(name, ...args); + } + + _formatErrorParams(err, job) { + const response = { + error: err, + worker: this.toJSON(), + }; + + if (job) response.job = formatJobObject(job); + return response; + } + + _claimJob(job) { + const m = moment(); + const startTime = m.toISOString(); + const expirationTime = m.add(job._source.timeout).toISOString(); + const attempts = job._source.attempts + 1; + + if (attempts > job._source.max_attempts) { + const msg = !job._source.output + ? `Max attempts reached (${job._source.max_attempts})` + : false; + return this._failJob(job, msg).then(() => false); + } + + const doc = { + attempts: attempts, + started_at: startTime, + process_expiration: expirationTime, + status: constants.JOB_STATUS_PROCESSING, + kibana_id: this.kibanaId, + kibana_name: this.kibanaName, + }; + + return this._client + .callAsInternalUser('update', { + index: job._index, + id: job._id, + if_seq_no: job._seq_no, + if_primary_term: job._primary_term, + body: { doc }, + }) + .then((response) => { + this.info(`Job marked as claimed: ${getUpdatedDocPath(response)}`); + const updatedJob = { + ...job, + ...response, + }; + updatedJob._source = { + ...job._source, + ...doc, + }; + return updatedJob; + }); + } + + _failJob(job, output = false) { + this.warn(`Failing job ${job._id}`); + + const completedTime = moment().toISOString(); + const docOutput = this._formatOutput(output); + const doc = { + status: constants.JOB_STATUS_FAILED, + completed_at: completedTime, + output: docOutput, + }; + + this.emit(constants.EVENT_WORKER_JOB_FAIL, { + job: formatJobObject(job), + worker: this.toJSON(), + output: docOutput, + }); + + return this._client + .callAsInternalUser('update', { + index: job._index, + id: job._id, + if_seq_no: job._seq_no, + if_primary_term: job._primary_term, + body: { doc }, + }) + .then((response) => { + this.info(`Job marked as failed: ${getUpdatedDocPath(response)}`); + }) + .catch((err) => { + if (err.statusCode === 409) return true; + this.error(`_failJob failed to update job ${job._id}`, err); + this.emit(constants.EVENT_WORKER_FAIL_UPDATE_ERROR, this._formatErrorParams(err, job)); + return false; + }); + } + + _formatOutput(output) { + const unknownMime = false; + const defaultOutput = null; + const docOutput = {}; + + if (typeof output === 'object' && output.content) { + docOutput.content = output.content; + docOutput.content_type = output.content_type || unknownMime; + docOutput.max_size_reached = output.max_size_reached; + docOutput.csv_contains_formulas = output.csv_contains_formulas; + docOutput.size = output.size; + docOutput.warnings = + output.warnings && output.warnings.length > 0 ? output.warnings : undefined; + } else { + docOutput.content = output || defaultOutput; + docOutput.content_type = unknownMime; + } + + return docOutput; + } + + _performJob(job) { + this.info(`Starting job`); + + const workerOutput = new Promise((resolve, reject) => { + // run the worker's workerFn + let isResolved = false; + const cancellationToken = new CancellationToken(); + const jobSource = job._source; + + Promise.resolve(this.workerFn.call(null, job, jobSource.payload, cancellationToken)) + .then((res) => { + // job execution was successful + if (res && res.warnings && res.warnings.length > 0) { + this.warn(`Job execution completed with warnings`); + } else { + this.info(`Job execution completed successfully`); + } + + isResolved = true; + resolve(res); + }) + .catch((err) => { + isResolved = true; + reject(err); + }); + + // fail if workerFn doesn't finish before timeout + const { timeout } = jobSource; + setTimeout(() => { + if (isResolved) return; + + cancellationToken.cancel(); + this.warn(`Timeout processing job ${job._id}`); + reject( + new WorkerTimeoutError(`Worker timed out, timeout = ${timeout}`, { + jobId: job._id, + timeout, + }) + ); + }, timeout); + }); + + return workerOutput.then( + (output) => { + const completedTime = moment().toISOString(); + const docOutput = this._formatOutput(output); + + const status = + output && output.warnings && output.warnings.length > 0 + ? constants.JOB_STATUS_WARNINGS + : constants.JOB_STATUS_COMPLETED; + const doc = { + status, + completed_at: completedTime, + output: docOutput, + }; + + return this._client + .callAsInternalUser('update', { + index: job._index, + id: job._id, + if_seq_no: job._seq_no, + if_primary_term: job._primary_term, + body: { doc }, + }) + .then((response) => { + const eventOutput = { + job: formatJobObject(job), + output: docOutput, + }; + this.emit(constants.EVENT_WORKER_COMPLETE, eventOutput); + + this.info(`Job data saved successfully: ${getUpdatedDocPath(response)}`); + }) + .catch((err) => { + if (err.statusCode === 409) return false; + this.error(`Failure saving job output ${job._id}`, err); + this.emit(constants.EVENT_WORKER_JOB_UPDATE_ERROR, this._formatErrorParams(err, job)); + return this._failJob(job, err.message ? err.message : false); + }); + }, + (jobErr) => { + if (!jobErr) { + jobErr = new UnspecifiedWorkerError('Unspecified worker error', { + jobId: job._id, + }); + } + + // job execution failed + if (jobErr.name === 'WorkerTimeoutError') { + this.warn(`Timeout on job ${job._id}`); + this.emit(constants.EVENT_WORKER_JOB_TIMEOUT, this._formatErrorParams(jobErr, job)); + return; + + // append the jobId to the error + } else { + try { + Object.assign(jobErr, { jobId: job._id }); + } catch (e) { + // do nothing if jobId can not be appended + } + } + + this.error(`Failure occurred on job ${job._id}`, jobErr); + this.emit(constants.EVENT_WORKER_JOB_EXECUTION_ERROR, this._formatErrorParams(jobErr, job)); + return this._failJob(job, jobErr.toString ? jobErr.toString() : false); + } + ); + } + + _startJobPolling() { + if (!this._running) { + return; + } + + this._poller.start(); + } + + _stopJobPolling() { + this._poller.stop(); + } + + _processPendingJobs() { + return this._getPendingJobs().then((jobs) => { + return this._claimPendingJobs(jobs); + }); + } + + _claimPendingJobs(jobs) { + if (!jobs || jobs.length === 0) return; + + let claimed = false; + + // claim a single job, stopping after first successful claim + return jobs + .reduce((chain, job) => { + return chain.then((claimedJob) => { + // short-circuit the promise chain if a job has been claimed + if (claimed) return claimedJob; + + return this._claimJob(job) + .then((claimResult) => { + claimed = true; + return claimResult; + }) + .catch((err) => { + if (err.statusCode === 409) { + this.warn( + `_claimPendingJobs encountered a version conflict on updating pending job ${job._id}`, + err + ); + return; // continue reducing and looking for a different job to claim + } + this.emit(constants.EVENT_WORKER_JOB_CLAIM_ERROR, this._formatErrorParams(err, job)); + return Promise.reject(err); + }); + }); + }, Promise.resolve()) + .then((claimedJob) => { + if (!claimedJob) { + this.debug(`Found no claimable jobs out of ${jobs.length} total`); + return; + } + return this._performJob(claimedJob); + }) + .catch((err) => { + this.error('Error claiming jobs', err); + return Promise.reject(err); + }); + } + + _getPendingJobs() { + const nowTime = moment().toISOString(); + const query = { + seq_no_primary_term: true, + _source: { + excludes: ['output.content'], + }, + query: { + bool: { + filter: { + bool: { + minimum_should_match: 1, + should: [ + { term: { status: 'pending' } }, + { + bool: { + must: [ + { term: { status: 'processing' } }, + { range: { process_expiration: { lte: nowTime } } }, + ], + }, + }, + ], + }, + }, + }, + }, + sort: [{ priority: { order: 'asc' } }, { created_at: { order: 'asc' } }], + size: constants.DEFAULT_WORKER_CHECK_SIZE, + }; + + return this._client + .callAsInternalUser('search', { + index: `${this.queue.index}-*`, + body: query, + }) + .then((results) => { + const jobs = results.hits.hits; + if (jobs.length > 0) { + this.debug(`${jobs.length} outstanding jobs returned`); + } + return jobs; + }) + .catch((err) => { + // ignore missing indices errors + if (err && err.status === 404) return []; + + this.error('job querying failed', err); + this.emit(constants.EVENT_WORKER_JOB_SEARCH_ERROR, this._formatErrorParams(err)); + throw err; + }); + } +} diff --git a/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/plugins/reporting/server/lib/export_types_registry.ts similarity index 91% rename from x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts rename to x-pack/plugins/reporting/server/lib/export_types_registry.ts index 0d5459a7c106b..893a2635561ff 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/plugins/reporting/server/lib/export_types_registry.ts @@ -6,10 +6,10 @@ import { isString } from 'lodash'; import memoizeOne from 'memoize-one'; -import { getExportType as getTypeCsv } from '../../export_types/csv'; -import { getExportType as getTypeCsvFromSavedObject } from '../../export_types/csv_from_savedobject'; -import { getExportType as getTypePng } from '../../export_types/png'; -import { getExportType as getTypePrintablePdf } from '../../export_types/printable_pdf'; +import { getExportType as getTypeCsv } from '../export_types/csv'; +import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_from_savedobject'; +import { getExportType as getTypePng } from '../export_types/png'; +import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf'; import { ExportTypeDefinition } from '../types'; type GetCallbackFn = ( diff --git a/x-pack/plugins/reporting/server/lib/get_user.ts b/x-pack/plugins/reporting/server/lib/get_user.ts new file mode 100644 index 0000000000000..49d15a7c55100 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/get_user.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { SecurityPluginSetup } from '../../../security/server'; + +export function getUserFactory(security?: SecurityPluginSetup) { + return (request: KibanaRequest) => { + return security?.authc.getCurrentUser(request) ?? null; + }; +} diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/index.ts rename to x-pack/plugins/reporting/server/lib/index.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/plugins/reporting/server/lib/jobs_query.ts similarity index 98% rename from x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts rename to x-pack/plugins/reporting/server/lib/jobs_query.ts index 06c4a7714099e..8784d8ff35d25 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/lib/jobs_query.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; -import { AuthenticatedUser } from '../../../../../plugins/security/server'; +import { AuthenticatedUser } from '../../../security/server'; import { ReportingConfig } from '../'; import { JobSource } from '../types'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts b/x-pack/plugins/reporting/server/lib/level_logger.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/level_logger.ts rename to x-pack/plugins/reporting/server/lib/level_logger.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/trace.ts b/x-pack/plugins/reporting/server/lib/trace.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/trace.ts rename to x-pack/plugins/reporting/server/lib/trace.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts b/x-pack/plugins/reporting/server/lib/validate/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/validate/index.ts rename to x-pack/plugins/reporting/server/lib/validate/index.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts b/x-pack/plugins/reporting/server/lib/validate/validate_browser.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts rename to x-pack/plugins/reporting/server/lib/validate/validate_browser.ts diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.test.js similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js rename to x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.test.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts rename to x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 905ed2b237c86..d0d25f6d9e0ae 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -6,33 +6,85 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { ConfigType, createConfig$ } from './config'; - -export interface PluginsSetup { - /** @deprecated */ - __legacy: { - config$: Observable; - }; -} +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { ReportingCore } from './core'; +import { ReportingConfigType } from './config'; +import { createBrowserDriverFactory } from './browsers'; +import { buildConfig, createConfig$ } from './config'; +import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; +import { registerRoutes } from './routes'; +import { setFieldFormats } from './services'; +import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; +import { registerReportingUsageCollector } from './usage'; -export class ReportingPlugin implements Plugin { - private readonly log: Logger; +export class ReportingPlugin + implements Plugin { + private readonly initializerContext: PluginInitializerContext; + private logger: LevelLogger; + private reportingCore?: ReportingCore; + private config$: Observable; - constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); + constructor(context: PluginInitializerContext) { + this.logger = new LevelLogger(context.logger.get()); + this.initializerContext = context; + this.config$ = context.config.create(); } - public async setup(core: CoreSetup): Promise { - return { - __legacy: { - config$: createConfig$(core, this.initializerContext, this.log).pipe(first()), - }, - }; + public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { + const { elasticsearch, http } = core; + const { licensing, security } = plugins; + const { initializerContext: initContext } = this; + const router = http.createRouter(); + const basePath = http.basePath.get; + + const coreConfig = await createConfig$(core, this.config$, this.logger) + .pipe(first()) + .toPromise(); // apply computed defaults to config + const reportingConfig = buildConfig(initContext, core, coreConfig); // combine kbnServer configs + this.reportingCore = new ReportingCore(reportingConfig); + + const browserDriverFactory = await createBrowserDriverFactory(reportingConfig, this.logger); + + this.reportingCore.pluginSetup({ + browserDriverFactory, + elasticsearch, + licensing, + basePath, + router, + security, + }); + + runValidations(reportingConfig, elasticsearch, browserDriverFactory, this.logger); + registerReportingUsageCollector(this.reportingCore, plugins); + registerRoutes(this.reportingCore, this.logger); + + return {}; } - public start() {} - public stop() {} -} + public async start(core: CoreStart, plugins: ReportingStartDeps) { + const { logger } = this; + const reportingCore = this.getReportingCore(); -export { ConfigType }; + const esqueue = await createQueueFactory(reportingCore, logger); + const enqueueJob = enqueueJobFactory(reportingCore, logger); + + reportingCore.pluginStart({ + savedObjects: core.savedObjects, + uiSettings: core.uiSettings, + esqueue, + enqueueJob, + }); + + setFieldFormats(plugins.data.fieldFormats); + logger.info('reporting plugin started'); + + return {}; + } + + public getReportingCore() { + if (!this.reportingCore) { + throw new Error('Setup is not ready'); + } + return this.reportingCore; + } +} diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts rename to x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts similarity index 96% rename from x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts rename to x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts index 4bc143b911572..b8326406743b7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; -import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; +import { getJobParamsFromRequest } from '../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; /* diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts similarity index 90% rename from x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts rename to x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 8a6d4553dfa9c..1221f67855410 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -6,14 +6,15 @@ import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; -import { HandlerErrorFunction } from './types'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; -import { createJobFactory, executeJobFactory } from '../../export_types/csv_from_savedobject'; -import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; -import { JobDocPayloadPanelCsv } from '../../export_types/csv_from_savedobject/types'; +import { createJobFactory } from '../export_types/csv_from_savedobject/server/create_job'; +import { executeJobFactory } from '../export_types/csv_from_savedobject/server/execute_job'; +import { getJobParamsFromRequest } from '../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; +import { JobDocPayloadPanelCsv } from '../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; import { JobDocOutput } from '../types'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { HandlerErrorFunction } from './types'; /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts similarity index 95% rename from x-pack/legacy/plugins/reporting/server/routes/generation.test.ts rename to x-pack/plugins/reporting/server/routes/generation.test.ts index 87ac71e250d0c..f9b3e5446cfce 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -6,10 +6,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; +import { setupServer } from 'src/core/server/test_utils'; import { registerJobGenerationRoutes } from './generation'; -import { createMockReportingCore } from '../../test_helpers'; +import { createMockReportingCore } from '../test_helpers'; import { ReportingCore } from '..'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { ExportTypeDefinition } from '../types'; diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/routes/generation.ts rename to x-pack/plugins/reporting/server/routes/generation.ts diff --git a/x-pack/legacy/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/routes/index.ts rename to x-pack/plugins/reporting/server/routes/index.ts diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts similarity index 98% rename from x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts rename to x-pack/plugins/reporting/server/routes/jobs.test.ts index 0911f48f82ca4..22d60d62d5fdb 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; -import { registerJobInfoRoutes } from './jobs'; -import { createMockReportingCore } from '../../test_helpers'; +import { of } from 'rxjs'; +import { setupServer } from 'src/core/server/test_utils'; +import supertest from 'supertest'; import { ReportingCore } from '..'; +import { ReportingInternalSetup } from '../core'; +import { LevelLogger } from '../lib'; import { ExportTypesRegistry } from '../lib/export_types_registry'; +import { createMockReportingCore } from '../test_helpers'; import { ExportTypeDefinition } from '../types'; -import { LevelLogger } from '../lib'; -import { ReportingInternalSetup } from '../core'; -import { of } from 'rxjs'; +import { registerJobInfoRoutes } from './jobs'; type setupServerReturn = UnwrapPromise>; diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/routes/jobs.ts rename to x-pack/plugins/reporting/server/routes/jobs.ts diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts similarity index 85% rename from x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts rename to x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index 4cb7af3d0d409..b218f9e4607dd 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -5,20 +5,23 @@ */ import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server'; -import sinon from 'sinon'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; -import { ReportingConfig, ReportingCore } from '../../'; -import { createMockReportingCore } from '../../../test_helpers'; +import { ReportingCore } from '../../'; +import { createMockReportingCore } from '../../test_helpers'; import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; import { ReportingInternalSetup } from '../../core'; -let mockConfig: ReportingConfig; let mockCore: ReportingCore; - -const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, -}); +const kbnConfig = { + 'server.basePath': '/sbp', +}; +const reportingConfig = { + 'roles.allow': ['reporting_user'], +}; +const mockReportingConfig = { + get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')] || 'whoah!', + kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, +}; const getMockContext = () => (({ @@ -38,13 +41,11 @@ const getMockResponseFactory = () => unauthorized: (obj: unknown) => obj, } as unknown) as KibanaResponseFactory); -beforeEach(async () => { - const mockConfigGet = sinon.stub().withArgs('roles', 'allow').returns(['reporting_user']); - mockConfig = getMockConfig(mockConfigGet); - mockCore = await createMockReportingCore(mockConfig); -}); - describe('authorized_user_pre_routing', function () { + beforeEach(async () => { + mockCore = await createMockReportingCore(mockReportingConfig); + }); + it('should return from handler with null user when security is disabled', async function () { mockCore.getPluginSetupDeps = () => (({ @@ -106,7 +107,7 @@ describe('authorized_user_pre_routing', function () { ).toMatchObject({ body: `Sorry, you don't have access to Reporting` }); }); - it('should return from handler when security is enabled and user has explicitly allowed role', async function () { + it('should return from handler when security is enabled and user has explicitly allowed role', async function (done) { mockCore.getPluginSetupDeps = () => (({ // @ts-ignore @@ -117,17 +118,16 @@ describe('authorized_user_pre_routing', function () { }, }, } as unknown) as ReportingInternalSetup); + // @ts-ignore overloading config getter + mockCore.config = mockReportingConfig; const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = getMockResponseFactory(); - let handlerCalled = false; - authorizedUserPreRouting((user: unknown) => { + authorizedUserPreRouting((user) => { expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); - handlerCalled = true; + done(); return Promise.resolve({ status: 200, options: {} }); })(getMockContext(), getMockRequest(), mockResponseFactory); - - expect(handlerCalled).toBe(true); }); it('should return from handler when security is enabled and user has superuser role', async function () {}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts similarity index 95% rename from x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts rename to x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 87582ca3ca239..2ad974c9dd8e1 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -5,7 +5,7 @@ */ import { RequestHandler, RouteMethod } from 'src/core/server'; -import { AuthenticatedUser } from '../../../../../../plugins/security/server'; +import { AuthenticatedUser } from '../../../../security/server'; import { getUserFactory } from '../../lib/get_user'; import { ReportingCore } from '../../core'; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts rename to x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts similarity index 97% rename from x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts rename to x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 990af2d0aca80..1a2e10cf355a2 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -5,7 +5,7 @@ */ import { ElasticsearchServiceSetup, kibanaResponseFactory } from 'kibana/server'; -import { AuthenticatedUser } from '../../../../../../plugins/security/server'; +import { AuthenticatedUser } from '../../../../security/server'; import { ReportingConfig } from '../../'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts new file mode 100644 index 0000000000000..5eceed0a7f2ab --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/types.d.ts @@ -0,0 +1,29 @@ +/* + * 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 { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'src/core/server'; +import { AuthenticatedUser } from '../../../security/server'; +import { JobDocPayload } from '../types'; + +export type HandlerFunction = ( + user: AuthenticatedUser | null, + exportType: string, + jobParams: object, + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory +) => any; + +export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any; + +export interface QueuedJobPayload { + error?: boolean; + source: { + job: { + payload: JobDocPayload; + }; + }; +} diff --git a/x-pack/plugins/reporting/server/services.ts b/x-pack/plugins/reporting/server/services.ts new file mode 100644 index 0000000000000..9f4897a69f4ed --- /dev/null +++ b/x-pack/plugins/reporting/server/services.ts @@ -0,0 +1,12 @@ +/* + * 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 { createGetterSetter } from '../../../../src/plugins/kibana_utils/server'; +import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; + +export const [getFieldFormats, setFieldFormats] = createGetterSetter< + DataPluginStart['fieldFormats'] +>('FieldFormats'); diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts similarity index 96% rename from x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts rename to x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index 260c94c31df1c..5b0d740e031ab 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -6,11 +6,11 @@ import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; +import { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; +import { createDriverFactory } from '../browsers/chromium'; import * as contexts from '../export_types/common/lib/screenshots/constants'; -import { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../server/browsers'; -import { createDriverFactory } from '../server/browsers/chromium'; -import { LevelLogger } from '../server/lib'; -import { CaptureConfig, ElementsPositionAndAttribute } from '../server/types'; +import { LevelLogger } from '../lib'; +import { CaptureConfig, ElementsPositionAndAttribute } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts similarity index 94% rename from x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts rename to x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts index 7f4330e7f6bc6..22da9eb418e9a 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts @@ -5,7 +5,7 @@ */ import { createLayout, LayoutInstance, LayoutTypes } from '../export_types/common/layouts'; -import { CaptureConfig } from '../server/types'; +import { CaptureConfig } from '../types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts new file mode 100644 index 0000000000000..b04e697d0a118 --- /dev/null +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -0,0 +1,80 @@ +/* + * 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. + */ + +jest.mock('../routes'); +jest.mock('../usage'); +jest.mock('../browsers'); +jest.mock('../browsers'); +jest.mock('../lib/create_queue'); +jest.mock('../lib/enqueue_job'); +jest.mock('../lib/validate'); + +import { of } from 'rxjs'; +import { coreMock } from 'src/core/server/mocks'; +import { ReportingConfig, ReportingCore } from '../'; +import { ReportingInternalSetup } from '../core'; +import { ReportingPlugin } from '../plugin'; +import { ReportingSetupDeps, ReportingStartDeps } from '../types'; + +const createMockSetupDeps = (setupMock?: any): ReportingSetupDeps => { + return { + security: setupMock.security, + licensing: { + license$: of({ isAvailable: true, isActive: true, type: 'basic' }), + } as any, + usageCollection: {} as any, + }; +}; + +export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({ + data: startMock.data, +}); + +const createMockReportingPlugin = async (config: ReportingConfig): Promise => { + const mockConfig = { + index: '.reporting', + kibanaServer: { + hostname: 'localhost', + port: '80', + }, + capture: { + browser: { + chromium: { + disableSandbox: true, + }, + }, + }, + ...config, + }; + const plugin = new ReportingPlugin(coreMock.createPluginInitializerContext(mockConfig)); + const setupMock = coreMock.createSetup(); + const coreStartMock = coreMock.createStart(); + const startMock = { + ...coreStartMock, + data: { fieldFormats: {} }, + }; + + await plugin.setup(setupMock, createMockSetupDeps(setupMock)); + await plugin.start(startMock, createMockStartDeps(startMock)); + + return plugin; +}; + +export const createMockReportingCore = async ( + config: ReportingConfig, + setupDepsMock?: ReportingInternalSetup +): Promise => { + config = config || {}; + const plugin = await createMockReportingPlugin(config); + const core = plugin.getReportingCore(); + + if (setupDepsMock) { + // @ts-ignore overwriting private properties + core.pluginSetupDeps = setupDepsMock; + } + + return core; +}; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_server.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts rename to x-pack/plugins/reporting/server/test_helpers/create_mock_server.ts diff --git a/x-pack/legacy/plugins/reporting/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/test_helpers/index.ts rename to x-pack/plugins/reporting/server/test_helpers/index.ts diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts new file mode 100644 index 0000000000000..409a89899bee0 --- /dev/null +++ b/x-pack/plugins/reporting/server/types.ts @@ -0,0 +1,215 @@ +/* + * 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 * as Rx from 'rxjs'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { DataPluginStart } from 'src/plugins/data/server/plugin'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CancellationToken } from '../../../plugins/reporting/common'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { JobStatus } from '../common/types'; +import { ReportingConfigType } from './config'; +import { ReportingCore } from './core'; +import { LayoutInstance } from './export_types/common/layouts'; +import { LevelLogger } from './lib'; + +/* + * Routing / API types + */ + +interface ListQuery { + page: string; + size: string; + ids?: string; // optional field forbids us from extending RequestQuery +} + +interface GenerateQuery { + jobParams: string; +} + +export type ReportingRequestQuery = ListQuery | GenerateQuery; + +export interface ReportingRequestPre { + management: { + jobTypes: any; + }; + user: string; +} + +// generate a report with unparsed jobParams +export interface GenerateExportTypePayload { + jobParams: string; +} + +export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPayload; + +export interface TimeRangeParams { + timezone: string; + min: Date | string | number | null; + max: Date | string | number | null; +} + +export interface JobParamPostPayload { + timerange: TimeRangeParams; +} + +export interface JobDocPayload { + headers?: string; // serialized encrypted headers + jobParams: JobParamsType; + title: string; + type: string | null; +} + +export interface JobSource { + _id: string; + _index: string; + _source: { + jobtype: string; + output: JobDocOutput; + payload: JobDocPayload; + status: JobStatus; + }; +} + +export interface JobDocOutput { + content_type: string; + content: string | null; + size: number; + max_size_reached?: boolean; + warnings?: string[]; +} + +interface ConditionalHeadersConditions { + protocol: string; + hostname: string; + port: number; + basePath: string; +} + +export interface ConditionalHeaders { + headers: Record; + conditions: ConditionalHeadersConditions; +} + +/* + * Screenshots + */ + +export interface ScreenshotObservableOpts { + logger: LevelLogger; + urls: string[]; + conditionalHeaders: ConditionalHeaders; + layout: LayoutInstance; + browserTimezone: string; +} + +export interface AttributesMap { + [key: string]: any; +} + +export interface ElementPosition { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: number; + left: number; + width: number; + height: number; + }; + scroll: { + x: number; + y: number; + }; +} + +export interface ElementsPositionAndAttribute { + position: ElementPosition; + attributes: AttributesMap; +} + +export interface Screenshot { + base64EncodedData: string; + title: string; + description: string; +} + +export interface ScreenshotResults { + timeRange: string | null; + screenshots: Screenshot[]; + error?: Error; + elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing +} + +export type ScreenshotsObservableFn = ({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, +}: ScreenshotObservableOpts) => Rx.Observable; + +/* + * Plugin Contract + */ + +export interface ReportingSetupDeps { + licensing: LicensingPluginSetup; + security?: SecurityPluginSetup; + usageCollection?: UsageCollectionSetup; +} + +export interface ReportingStartDeps { + data: DataPluginStart; +} + +export type ReportingStart = object; +export type ReportingSetup = object; + +/* + * Internal Types + */ + +export type ESQueueCreateJobFn = ( + jobParams: JobParamsType, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + +export type ESQueueWorkerExecuteFn = ( + jobId: string, + job: JobDocPayloadType, + cancellationToken?: CancellationToken +) => Promise; + +export type CaptureConfig = ReportingConfigType['capture']; +export type ScrollConfig = ReportingConfigType['csv']['scroll']; + +export type CreateJobFactory = ( + reporting: ReportingCore, + logger: LevelLogger +) => CreateJobFnType; + +export type ExecuteJobFactory = ( + reporting: ReportingCore, + logger: LevelLogger +) => Promise; // FIXME: does not "need" to be async + +export interface ExportTypeDefinition< + JobParamsType, + CreateJobFnType, + JobPayloadType, + ExecuteJobFnType +> { + id: string; + name: string; + jobType: string; + jobContentEncoding?: string; + jobContentExtension: string; + createJobFactory: CreateJobFactory; + executeJobFactory: ExecuteJobFactory; + validLicenses: string[]; +} diff --git a/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap rename to x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap diff --git a/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts b/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts rename to x-pack/plugins/reporting/server/usage/decorate_range_stats.ts diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts b/x-pack/plugins/reporting/server/usage/get_export_type_handler.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts rename to x-pack/plugins/reporting/server/usage/get_export_type_handler.ts diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts rename to x-pack/plugins/reporting/server/usage/get_reporting_usage.ts diff --git a/x-pack/plugins/reporting/server/usage/index.ts b/x-pack/plugins/reporting/server/usage/index.ts new file mode 100644 index 0000000000000..a426451db2282 --- /dev/null +++ b/x-pack/plugins/reporting/server/usage/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { LicenseType } from '../../../licensing/server'; + +export interface FeaturesAvailability { + isAvailable: () => boolean; + license: { + getType: () => LicenseType | undefined; + }; +} +export type GetLicense = () => Promise; +export { registerReportingUsageCollector } from './reporting_usage_collector'; diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts similarity index 99% rename from x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts rename to x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 830c6275cd96a..d5dccaca3042a 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -8,7 +8,7 @@ import * as Rx from 'rxjs'; import sinon from 'sinon'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingConfig } from '../'; -import { createMockReportingCore } from '../../test_helpers'; +import { createMockReportingCore } from '../test_helpers'; import { getExportTypesRegistry } from '../lib/export_types_registry'; import { ReportingSetupDeps } from '../types'; import { FeaturesAvailability } from './'; diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts rename to x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts diff --git a/x-pack/legacy/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts similarity index 100% rename from x-pack/legacy/plugins/reporting/server/usage/types.ts rename to x-pack/plugins/reporting/server/usage/types.ts diff --git a/x-pack/plugins/rollup/public/application.tsx b/x-pack/plugins/rollup/public/application.tsx index 1bdf940d746b2..16a0312341118 100644 --- a/x-pack/plugins/rollup/public/application.tsx +++ b/x-pack/plugins/rollup/public/application.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { ChromeBreadcrumb, CoreSetup } from 'kibana/public'; +import { CoreSetup } from 'kibana/public'; import { render, unmountComponentAtNode } from 'react-dom'; import { Provider } from 'react-redux'; import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; @@ -16,28 +16,28 @@ import { App } from './crud_app/app'; import './index.scss'; +import { ManagementAppMountParams } from '../../../../src/plugins/management/public'; + /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ export const renderApp = async ( core: CoreSetup, - { - element, - setBreadcrumbs, - }: { element: HTMLElement; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void } + { history, element, setBreadcrumbs }: ManagementAppMountParams ) => { const [coreStart] = await core.getStartServices(); const I18nContext = coreStart.i18n.Context; + const services = { + history, + setBreadcrumbs, + }; + render( - + - + , diff --git a/x-pack/plugins/rollup/public/crud_app/app.js b/x-pack/plugins/rollup/public/crud_app/app.js index 0ef3253eeb94e..4eff849776aef 100644 --- a/x-pack/plugins/rollup/public/crud_app/app.js +++ b/x-pack/plugins/rollup/public/crud_app/app.js @@ -6,10 +6,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { HashRouter, Switch, Route, Redirect, withRouter } from 'react-router-dom'; +import { Router, Switch, Route, Redirect, withRouter } from 'react-router-dom'; import { UIM_APP_LOAD } from '../../common'; -import { CRUD_APP_BASE_PATH } from './constants'; import { registerRouter, setUserHasLeftApp, METRIC_TYPE } from './services'; import { trackUiMetric } from '../kibana_services'; import { JobList, JobCreate } from './sections'; @@ -53,15 +52,15 @@ export class App extends Component { render() { return ( - + - - - + + + - + ); } } diff --git a/x-pack/plugins/rollup/public/crud_app/constants/index.js b/x-pack/plugins/rollup/public/crud_app/constants/index.js index f3a218fc3b493..132affafea87d 100644 --- a/x-pack/plugins/rollup/public/crud_app/constants/index.js +++ b/x-pack/plugins/rollup/public/crud_app/constants/index.js @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { CRUD_APP_BASE_PATH } from './paths'; - export { METRICS_CONFIG } from './metrics_config'; diff --git a/x-pack/plugins/rollup/public/crud_app/constants/paths.js b/x-pack/plugins/rollup/public/crud_app/constants/paths.js deleted file mode 100644 index 44829f38e79cd..0000000000000 --- a/x-pack/plugins/rollup/public/crud_app/constants/paths.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const CRUD_APP_BASE_PATH = '/management/data/rollup_jobs'; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js index 011becded148c..85cd6e742d27f 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js @@ -27,7 +27,6 @@ import { import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { CRUD_APP_BASE_PATH } from '../../constants'; import { getRouterLinkProps, extractQueryParams, listBreadcrumb } from '../../services'; import { JobTable } from './job_table'; @@ -166,7 +165,7 @@ export class JobListUi extends Component { actions={ @@ -210,7 +209,7 @@ export class JobListUi extends Component { {this.getHeaderSection()} - + async (dispatch) => { // This will open the new job in the detail panel. Note that we're *not* showing a success toast // here, because it would partially obscure the detail panel. getRouter().history.push({ - pathname: `${CRUD_APP_BASE_PATH}/job_list`, + pathname: `/job_list`, search: `?job=${jobConfig.id}`, }); }; diff --git a/x-pack/plugins/rollup/public/crud_app/store/middleware/clone_job.js b/x-pack/plugins/rollup/public/crud_app/store/middleware/clone_job.js index 2012ec2248fbd..b8495a1c95a8e 100644 --- a/x-pack/plugins/rollup/public/crud_app/store/middleware/clone_job.js +++ b/x-pack/plugins/rollup/public/crud_app/store/middleware/clone_job.js @@ -6,7 +6,6 @@ import { getRouter, getUserHasLeftApp } from '../../services'; import { CLONE_JOB_START } from '../action_types'; -import { CRUD_APP_BASE_PATH } from '../../constants'; export const cloneJob = () => (next) => (action) => { const { type } = action; @@ -14,7 +13,7 @@ export const cloneJob = () => (next) => (action) => { if (type === CLONE_JOB_START) { if (!getUserHasLeftApp()) { getRouter().history.push({ - pathname: `${CRUD_APP_BASE_PATH}/create`, + pathname: `/create`, }); } } diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts index b2e793d7e75e9..b55760c5cc5aa 100644 --- a/x-pack/plugins/rollup/public/plugin.ts +++ b/x-pack/plugins/rollup/public/plugin.ts @@ -16,8 +16,6 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -// @ts-ignore -import { CRUD_APP_BASE_PATH } from './crud_app/constants'; import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; import { IndexPatternManagementSetup } from '../../../../src/plugins/index_pattern_management/public'; @@ -71,7 +69,7 @@ export class RollupPlugin implements Plugin { 'Summarize and store historical data in a smaller index for future analysis.', }), icon: 'indexRollupApp', - path: `#${CRUD_APP_BASE_PATH}/job_list`, + path: `/app/management/data/rollup_jobs/job_list`, showOnHomePage: true, category: FeatureCatalogueCategory.ADMIN, }); diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js index 380275df05ba8..53a3af38f3235 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js @@ -8,7 +8,6 @@ import { mockHttpRequest, pageHelpers, nextTick } from './helpers'; import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; import { getRouter } from '../../crud_app/services/routing'; import { setHttp } from '../../crud_app/services'; -import { CRUD_APP_BASE_PATH } from '../../crud_app/constants'; import { coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash/function/debounce', () => (fn) => fn); @@ -65,8 +64,8 @@ describe('Smoke test cloning an existing rollup job from job list', () => { find('jobActionMenuButton').simulate('click'); - expect(router.history.location.pathname).not.toBe(`${CRUD_APP_BASE_PATH}/create`); + expect(router.history.location.pathname).not.toBe(`/create`); find('jobCloneActionContextMenu').simulate('click'); - expect(router.history.location.pathname).toBe(`${CRUD_APP_BASE_PATH}/create`); + expect(router.history.location.pathname).toBe(`/create`); }); }); diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts index 77e6460b7669a..564b71a2e0fac 100644 --- a/x-pack/plugins/security/common/licensing/license_service.test.ts +++ b/x-pack/plugins/security/common/licensing/license_service.test.ts @@ -5,7 +5,7 @@ */ import { of, BehaviorSubject } from 'rxjs'; -import { licensingMock } from '../../../licensing/public/mocks'; +import { licenseMock } from '../../../licensing/common/licensing.mock'; import { SecurityLicenseService } from './license_service'; describe('license features', function () { @@ -29,7 +29,7 @@ describe('license features', function () { }); it('should display error when X-Pack is unavailable', () => { - const rawLicenseMock = licensingMock.createLicenseMock(); + const rawLicenseMock = licenseMock.createLicenseMock(); rawLicenseMock.isAvailable = false; const serviceSetup = new SecurityLicenseService().setup({ license$: of(rawLicenseMock), @@ -50,7 +50,7 @@ describe('license features', function () { }); it('should notify consumers of licensed feature changes', () => { - const rawLicenseMock = licensingMock.createLicenseMock(); + const rawLicenseMock = licenseMock.createLicenseMock(); rawLicenseMock.isAvailable = false; const rawLicense$ = new BehaviorSubject(rawLicenseMock); const serviceSetup = new SecurityLicenseService().setup({ @@ -79,7 +79,7 @@ describe('license features', function () { ] `); - rawLicense$.next(licensingMock.createLicenseMock()); + rawLicense$.next(licenseMock.createLicenseMock()); expect(subscriptionHandler).toHaveBeenCalledTimes(2); expect(subscriptionHandler.mock.calls[1]).toMatchInlineSnapshot(` Array [ @@ -103,7 +103,7 @@ describe('license features', function () { }); it('should show login page and other security elements, allow RBAC but forbid paid features if license is basic.', () => { - const mockRawLicense = licensingMock.createLicense({ + const mockRawLicense = licenseMock.createLicense({ features: { security: { isEnabled: true, isAvailable: true } }, }); @@ -129,7 +129,7 @@ describe('license features', function () { }); it('should not show login page or other security elements if security is disabled in Elasticsearch.', () => { - const mockRawLicense = licensingMock.createLicense({ + const mockRawLicense = licenseMock.createLicense({ features: { security: { isEnabled: false, isAvailable: true } }, }); @@ -151,7 +151,7 @@ describe('license features', function () { }); it('should allow role mappings, access agreement and sub-feature privileges, but not DLS/FLS if license = gold', () => { - const mockRawLicense = licensingMock.createLicense({ + const mockRawLicense = licenseMock.createLicense({ license: { mode: 'gold', type: 'gold' }, features: { security: { isEnabled: true, isAvailable: true } }, }); @@ -174,7 +174,7 @@ describe('license features', function () { }); it('should allow to login, allow RBAC, role mappings, access agreement, sub-feature privileges, and DLS if license >= platinum', () => { - const mockRawLicense = licensingMock.createLicense({ + const mockRawLicense = licenseMock.createLicense({ license: { mode: 'platinum', type: 'platinum' }, features: { security: { isEnabled: true, isAvailable: true } }, }); @@ -197,7 +197,7 @@ describe('license features', function () { }); it('should allow all basic features + audit logging for standard license', () => { - const mockRawLicense = licensingMock.createLicense({ + const mockRawLicense = licenseMock.createLicense({ license: { mode: 'standard', type: 'standard' }, features: { security: { isEnabled: true, isAvailable: true } }, }); diff --git a/x-pack/plugins/security/public/account_management/account_management_app.ts b/x-pack/plugins/security/public/account_management/account_management_app.ts index 41567a04fe030..0bb7785259c0e 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.ts +++ b/x-pack/plugins/security/public/account_management/account_management_app.ts @@ -5,7 +5,12 @@ */ import { i18n } from '@kbn/i18n'; -import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public'; +import { + ApplicationSetup, + AppMountParameters, + AppNavLinkStatus, + StartServicesAccessor, +} from '../../../../../src/core/public'; import { AuthenticationServiceSetup } from '../authentication'; interface CreateDeps { @@ -23,8 +28,7 @@ export const accountManagementApp = Object.freeze({ application.register({ id: this.id, title, - // TODO: switch to proper enum once https://github.com/elastic/kibana/issues/58327 is resolved. - navLinkStatus: 3, + navLinkStatus: AppNavLinkStatus.hidden, appRoute: '/security/account', async mount({ element }: AppMountParameters) { const [ diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss index 6784052ef4337..344cde9c7825c 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss @@ -23,9 +23,10 @@ } &:focus { + @include euiFocusRing; + border-color: transparent; border-radius: $euiBorderRadius; - @include euiFocusRing; .secLoginCard__title { text-decoration: underline; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index 0500abcd2206a..94f9de010cc2a 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -64,10 +64,13 @@ describe('APIKeysGridPage', () => { }); }); + const coreStart = coreMock.createStart(); + const getViewProperties = () => { - const { docLinks, notifications } = coreMock.createStart(); + const { docLinks, notifications, application } = coreStart; return { docLinks: new DocumentationLinksService(docLinks), + navigateToApp: application.navigateToApp, notifications, apiKeysAPIClient: apiClientMock, }; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 8308a66e2d900..1ee1adf41a156 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -26,7 +26,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; -import { NotificationsStart } from 'src/core/public'; +import { ApplicationStart, NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; import { APIKeysAPIClient } from '../api_keys_api_client'; @@ -40,6 +40,7 @@ interface Props { notifications: NotificationsStart; docLinks: DocumentationLinksService; apiKeysAPIClient: PublicMethodsOf; + navigateToApp: ApplicationStart['navigateToApp']; } interface State { @@ -137,7 +138,11 @@ export class APIKeysGridPage extends Component { if (!isLoadingTable && apiKeys && apiKeys.length === 0) { return ( - + ); } diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx index ef1ac40ca4b32..9b2ccfcb99ef3 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx @@ -5,6 +5,8 @@ */ import React, { Fragment } from 'react'; +import { ApplicationStart } from 'kibana/public'; + import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { DocumentationLinksService } from '../../documentation_links'; @@ -12,9 +14,14 @@ import { DocumentationLinksService } from '../../documentation_links'; interface Props { isAdmin: boolean; docLinks: DocumentationLinksService; + navigateToApp: ApplicationStart['navigateToApp']; } -export const EmptyPrompt: React.FunctionComponent = ({ isAdmin, docLinks }) => ( +export const EmptyPrompt: React.FunctionComponent = ({ + isAdmin, + docLinks, + navigateToApp, +}) => ( = ({ isAdmin, docLinks } actions={ - + navigateToApp('dev_tools')} + data-test-subj="goToConsoleButton" + > ({ APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, })); - +import { ScopedHistory } from 'src/core/public'; import { apiKeysManagementApp } from './api_keys_management_app'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; describe('apiKeysManagementApp', () => { it('create() returns proper management app descriptor', () => { @@ -37,10 +37,11 @@ describe('apiKeysManagementApp', () => { basePath: '/some-base-path', element: container, setBreadcrumbs, + history: (scopedHistoryMock.create() as unknown) as ScopedHistory, }); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '#/some-base-path', text: 'API Keys' }]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '/', text: 'API Keys' }]); expect(container).toMatchInlineSnapshot(`
Page: {"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"apiKeysAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}}} diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index b9ec5b35b3f9d..6ff91852d0a3e 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -25,18 +25,18 @@ export const apiKeysManagementApp = Object.freeze({ title: i18n.translate('xpack.security.management.apiKeysTitle', { defaultMessage: 'API Keys', }), - async mount({ basePath, element, setBreadcrumbs }) { + async mount({ element, setBreadcrumbs }) { setBreadcrumbs([ { text: i18n.translate('xpack.security.apiKeys.breadcrumb', { defaultMessage: 'API Keys', }), - href: `#${basePath}`, + href: `/`, }, ]); const [ - [{ docLinks, http, notifications, i18n: i18nStart }], + [{ docLinks, http, notifications, i18n: i18nStart, application }], { APIKeysGridPage }, { APIKeysAPIClient }, ] = await Promise.all([ @@ -48,6 +48,7 @@ export const apiKeysManagementApp = Object.freeze({ render( { describe('setup()', () => { it('properly registers security section and its applications', () => { @@ -24,11 +30,10 @@ describe('ManagementService', () => { const { authc } = securityMock.createSetup(); const license = licenseMock.create(); - const mockSection = { registerApp: jest.fn() }; - const managementSetup = { + const managementSetup: ManagementSetup = { sections: { + register: jest.fn(), getSection: jest.fn().mockReturnValue(mockSection), - getAllSections: jest.fn(), }, }; @@ -80,17 +85,20 @@ describe('ManagementService', () => { license.features$ = licenseSubject; const service = new ManagementService(); + + const managementSetup: ManagementSetup = { + sections: { + register: jest.fn(), + getSection: jest.fn().mockReturnValue(mockSection), + }, + }; + service.setup({ getStartServices: getStartServices as any, license, fatalErrors, authc: securityMock.createSetup().authc, - management: { - sections: { - getSection: jest.fn().mockReturnValue({ registerApp: jest.fn() }), - getAllSections: jest.fn(), - }, - }, + management: managementSetup, }); const getMockedApp = () => { @@ -115,17 +123,18 @@ describe('ManagementService', () => { [roleMappingsManagementApp.id, getMockedApp()], ] as Array<[string, jest.Mocked]>); - service.start({ - management: { - sections: { - getSection: jest - .fn() - .mockReturnValue({ getApp: jest.fn().mockImplementation((id) => mockApps.get(id)) }), - getAllSections: jest.fn(), - navigateToApp: jest.fn(), - }, - legacy: undefined, + const managementStart: ManagementStart = { + sections: { + getSection: jest + .fn() + .mockReturnValue({ getApp: jest.fn().mockImplementation((id) => mockApps.get(id)) }), + getAllSections: jest.fn(), + getSectionsEnabled: jest.fn(), }, + }; + + service.start({ + management: managementStart, }); return { diff --git a/x-pack/plugins/security/public/management/management_urls.ts b/x-pack/plugins/security/public/management/management_urls.ts index 0d4e3fc920bdb..493f6c64317a5 100644 --- a/x-pack/plugins/security/public/management/management_urls.ts +++ b/x-pack/plugins/security/public/management/management_urls.ts @@ -4,20 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -const MANAGEMENT_PATH = '/management'; -const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; -export const ROLES_PATH = `${SECURITY_PATH}/roles`; -export const EDIT_ROLES_PATH = `${ROLES_PATH}/edit`; -export const CLONE_ROLES_PATH = `${ROLES_PATH}/clone`; -export const USERS_PATH = `${SECURITY_PATH}/users`; -export const EDIT_USERS_PATH = `${USERS_PATH}/edit`; -export const ROLE_MAPPINGS_PATH = `${SECURITY_PATH}/role_mappings`; -const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; - -export const getEditRoleHref = (roleName: string) => - `#${ROLES_PATH}/edit/${encodeURIComponent(roleName)}`; - -export const getCreateRoleMappingHref = () => `#${CREATE_ROLE_MAPPING_PATH}`; +export const EDIT_ROLE_MAPPING_PATH = `/edit`; export const getEditRoleMappingHref = (roleMappingName: string) => - `#${CREATE_ROLE_MAPPING_PATH}/${encodeURIComponent(roleMappingName)}`; + `${EDIT_ROLE_MAPPING_PATH}/${encodeURIComponent(roleMappingName)}`; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx index eea6bbef94306..b4e755507f8c5 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx @@ -12,6 +12,7 @@ import { findTestSubject } from 'test_utils/find_test_subject'; // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. import 'test_utils/stub_web_worker'; +import { ScopedHistory } from 'kibana/public'; import { EditRoleMappingPage } from '.'; import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../components'; @@ -21,13 +22,15 @@ import { RolesAPIClient } from '../../roles'; import { Role } from '../../../../common/model'; import { DocumentationLinksService } from '../documentation_links'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; import { rolesAPIClientMock } from '../../roles/roles_api_client.mock'; import { RoleComboBox } from '../../role_combo_box'; describe('EditRoleMappingPage', () => { + const history = (scopedHistoryMock.create() as unknown) as ScopedHistory; let rolesAPI: PublicMethodsOf; + beforeEach(() => { rolesAPI = rolesAPIClientMock.create(); (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ @@ -54,6 +57,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); @@ -116,6 +120,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); @@ -163,6 +168,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); expect(wrapper.find(SectionLoading)).toHaveLength(1); @@ -190,6 +196,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); expect(wrapper.find(SectionLoading)).toHaveLength(1); @@ -227,6 +234,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); @@ -267,6 +275,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); @@ -309,6 +318,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); @@ -363,6 +373,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); @@ -418,6 +429,7 @@ describe('EditRoleMappingPage', () => { rolesAPIClient={rolesAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} /> ); diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx index 3b16bd8dc44ac..b4e3627039ecb 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NotificationsStart } from 'src/core/public'; +import { NotificationsStart, ScopedHistory } from 'src/core/public'; import { RoleMapping } from '../../../../common/model'; import { RuleEditorPanel } from './rule_editor_panel'; import { @@ -29,7 +29,6 @@ import { SectionLoading, } from '../components'; import { RolesAPIClient } from '../../roles'; -import { ROLE_MAPPINGS_PATH } from '../../management_urls'; import { validateRoleMappingForSave } from './services/role_mapping_validation'; import { MappingInfoPanel } from './mapping_info_panel'; import { DocumentationLinksService } from '../documentation_links'; @@ -55,6 +54,7 @@ interface Props { rolesAPIClient: PublicMethodsOf; notifications: NotificationsStart; docLinks: DocumentationLinksService; + history: ScopedHistory; } export class EditRoleMappingPage extends Component { @@ -342,7 +342,5 @@ export class EditRoleMappingPage extends Component { } } - private backToRoleMappingsList = () => { - window.location.hash = ROLE_MAPPINGS_PATH; - }; + private backToRoleMappingsList = () => this.props.history.push('/'); } diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx index 6fe4bcc7a0bbb..7330dba968162 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx @@ -7,11 +7,21 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getCreateRoleMappingHref } from '../../../management_urls'; +import { ScopedHistory } from 'kibana/public'; +import { EDIT_ROLE_MAPPING_PATH } from '../../../management_urls'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; -export const CreateRoleMappingButton = () => { +interface CreateRoleMappingButtonProps { + history: ScopedHistory; +} + +export const CreateRoleMappingButton = ({ history }: CreateRoleMappingButtonProps) => { return ( - + = () => ( +interface EmptyPromptProps { + history: ScopedHistory; +} + +export const EmptyPrompt: React.FunctionComponent = ({ history }) => ( = () => (

} - actions={} + actions={} data-test-subj="roleMappingsEmptyPrompt" /> ); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx index 0d343ad33d78e..fb81ddb641e1f 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { CoreStart, ScopedHistory } from 'kibana/public'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { RoleMappingsGridPage } from '.'; import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../components'; @@ -14,11 +15,19 @@ import { EuiLink } from '@elastic/eui'; import { act } from '@testing-library/react'; import { DocumentationLinksService } from '../documentation_links'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; import { rolesAPIClientMock } from '../../roles/index.mock'; describe('RoleMappingsGridPage', () => { + let history: ScopedHistory; + let coreStart: CoreStart; + + beforeEach(() => { + history = (scopedHistoryMock.create() as unknown) as ScopedHistory; + coreStart = coreMock.createStart(); + }); + it('renders an empty prompt when no role mappings exist', async () => { const roleMappingsAPI = roleMappingsAPIClientMock.create(); roleMappingsAPI.getRoleMappings.mockResolvedValue([]); @@ -34,6 +43,8 @@ describe('RoleMappingsGridPage', () => { roleMappingsAPI={roleMappingsAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); expect(wrapper.find(SectionLoading)).toHaveLength(1); @@ -61,6 +72,8 @@ describe('RoleMappingsGridPage', () => { roleMappingsAPI={roleMappingsAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); expect(wrapper.find(SectionLoading)).toHaveLength(1); @@ -96,6 +109,8 @@ describe('RoleMappingsGridPage', () => { roleMappingsAPI={roleMappingsAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); expect(wrapper.find(SectionLoading)).toHaveLength(1); @@ -130,6 +145,8 @@ describe('RoleMappingsGridPage', () => { roleMappingsAPI={roleMappingsAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); await nextTick(); @@ -137,9 +154,7 @@ describe('RoleMappingsGridPage', () => { const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); expect(links).toHaveLength(1); - expect(links.at(0).props()).toMatchObject({ - href: '#/management/security/roles/edit/superuser', - }); + expect(links.at(0).props().onClick).toBeDefined(); }); it('describes the number of mapped role templates', async () => { @@ -164,6 +179,8 @@ describe('RoleMappingsGridPage', () => { roleMappingsAPI={roleMappingsAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); await nextTick(); @@ -202,6 +219,8 @@ describe('RoleMappingsGridPage', () => { roleMappingsAPI={roleMappingsAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); await nextTick(); @@ -263,6 +282,8 @@ describe('RoleMappingsGridPage', () => { roleMappingsAPI={roleMappingsAPI} notifications={notifications} docLinks={new DocumentationLinksService(docLinks)} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); await nextTick(); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index d0bc96b4fcedf..757e59a4e0583 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -24,7 +24,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NotificationsStart } from 'src/core/public'; +import { NotificationsStart, ApplicationStart, ScopedHistory } from 'src/core/public'; import { RoleMapping, Role } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; import { @@ -33,18 +33,21 @@ import { PermissionDenied, SectionLoading, } from '../components'; -import { getCreateRoleMappingHref, getEditRoleMappingHref } from '../../management_urls'; +import { EDIT_ROLE_MAPPING_PATH, getEditRoleMappingHref } from '../../management_urls'; import { DocumentationLinksService } from '../documentation_links'; import { RoleMappingsAPIClient } from '../role_mappings_api_client'; import { RoleTableDisplay } from '../../role_table_display'; import { RolesAPIClient } from '../../roles'; import { EnabledBadge, DisabledBadge } from '../../badges'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { rolesAPIClient: PublicMethodsOf; roleMappingsAPI: PublicMethodsOf; notifications: NotificationsStart; docLinks: DocumentationLinksService; + history: ScopedHistory; + navigateToApp: ApplicationStart['navigateToApp']; } interface State { @@ -119,7 +122,7 @@ export class RoleMappingsGridPage extends Component { if (loadState === 'finished' && roleMappings && roleMappings.length === 0) { return ( - + ); } @@ -160,7 +163,10 @@ export class RoleMappingsGridPage extends Component { - + { render: (roleMappingName: string) => { return ( {roleMappingName} @@ -323,7 +329,13 @@ export class RoleMappingsGridPage extends Component { const role: Role | string = this.state.roles?.find((r) => r.name === rolename) ?? rolename; - return ; + return ( + + ); }); return
{roleLinks}
; }, @@ -367,7 +379,10 @@ export class RoleMappingsGridPage extends Component { iconType="pencil" color="primary" data-test-subj={`editRoleMappingButton-${record.name}`} - href={getEditRoleMappingHref(record.name)} + {...reactRouterNavigate( + this.props.history, + getEditRoleMappingHref(record.name) + )} /> ); 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 5907413d7299e..c95d78f90f51a 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 @@ -12,17 +12,22 @@ jest.mock('./edit_role_mapping', () => ({ EditRoleMappingPage: (props: any) => `Role Mapping Edit Page: ${JSON.stringify(props)}`, })); +import { ScopedHistory } from 'src/core/public'; import { roleMappingsManagementApp } from './role_mappings_management_app'; +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; -import { coreMock } from '../../../../../../src/core/public/mocks'; - -async function mountApp(basePath: string) { +async function mountApp(basePath: string, pathname: string) { const container = document.createElement('div'); const setBreadcrumbs = jest.fn(); const unmount = await roleMappingsManagementApp .create({ getStartServices: coreMock.createSetup().getStartServices as any }) - .mount({ basePath, element: container, setBreadcrumbs }); + .mount({ + basePath, + element: container, + setBreadcrumbs, + history: (scopedHistoryMock.create({ pathname }) as unknown) as ScopedHistory, + }); return { unmount, container, setBreadcrumbs }; } @@ -44,16 +49,13 @@ describe('roleMappingsManagementApp', () => { }); it('mount() works for the `grid` page', async () => { - const basePath = '/some-base-path/role_mappings'; - window.location.hash = basePath; - - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', '/'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Role Mappings' }]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Role Mappings' }]); expect(container).toMatchInlineSnapshot(`
- Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} + Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}}
`); @@ -63,19 +65,16 @@ describe('roleMappingsManagementApp', () => { }); it('mount() works for the `create role mapping` page', async () => { - const basePath = '/some-base-path/role_mappings'; - window.location.hash = `${basePath}/edit`; - - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', '/edit'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Role Mappings' }, + { href: `/`, text: 'Role Mappings' }, { text: 'Create' }, ]); expect(container).toMatchInlineSnapshot(`
- Role Mapping Edit Page: {"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/"}} + Role Mapping Edit Page: {"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","search":"","hash":""}}}
`); @@ -85,20 +84,18 @@ describe('roleMappingsManagementApp', () => { }); it('mount() works for the `edit role mapping` page', async () => { - const basePath = '/some-base-path/role_mappings'; const roleMappingName = 'someRoleMappingName'; - window.location.hash = `${basePath}/edit/${roleMappingName}`; - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${roleMappingName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Role Mappings' }, - { href: `#/some-base-path/role_mappings/edit/${roleMappingName}`, text: roleMappingName }, + { href: `/`, text: 'Role Mappings' }, + { href: `/edit/${roleMappingName}`, text: roleMappingName }, ]); expect(container).toMatchInlineSnapshot(`
- 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/"}} + 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":""}}}
`); @@ -108,18 +105,15 @@ describe('roleMappingsManagementApp', () => { }); it('mount() properly encodes role mapping name in `edit role mapping` page link in breadcrumbs', async () => { - const basePath = '/some-base-path/role_mappings'; const roleMappingName = 'some 安全性 role mapping'; - window.location.hash = `${basePath}/edit/${roleMappingName}`; - const { setBreadcrumbs } = await mountApp(basePath); + const { setBreadcrumbs } = await mountApp('/', `/edit/${roleMappingName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Role Mappings' }, + { href: `/`, text: 'Role Mappings' }, { - href: - '#/some-base-path/role_mappings/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role%20mapping', + href: '/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role%20mapping', text: roleMappingName, }, ]); 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 ffb6d6d98f180..bca3a070e64f9 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 @@ -6,7 +6,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { Router, Route, Switch, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; @@ -26,13 +26,14 @@ export const roleMappingsManagementApp = Object.freeze({ title: i18n.translate('xpack.security.management.roleMappingsTitle', { defaultMessage: 'Role Mappings', }), - async mount({ basePath, element, setBreadcrumbs }) { + async mount({ element, setBreadcrumbs, history }) { + const [coreStart] = await getStartServices(); const roleMappingsBreadcrumbs = [ { text: i18n.translate('xpack.security.roleMapping.breadcrumb', { defaultMessage: 'Role Mappings', }), - href: `#${basePath}`, + href: `/`, }, ]; @@ -60,6 +61,8 @@ export const roleMappingsManagementApp = Object.freeze({ rolesAPIClient={new RolesAPIClient(http)} roleMappingsAPI={roleMappingsAPIClient} docLinks={dockLinksService} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); }; @@ -70,7 +73,7 @@ export const roleMappingsManagementApp = Object.freeze({ setBreadcrumbs([ ...roleMappingsBreadcrumbs, name - ? { text: name, href: `#${basePath}/edit/${encodeURIComponent(name)}` } + ? { text: name, href: `/edit/${encodeURIComponent(name)}` } : { text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { defaultMessage: 'Create', @@ -85,15 +88,16 @@ export const roleMappingsManagementApp = Object.freeze({ rolesAPIClient={new RolesAPIClient(http)} notifications={notifications} docLinks={dockLinksService} + history={history} /> ); }; render( - + - + diff --git a/x-pack/plugins/security/public/management/role_table_display/role_table_display.tsx b/x-pack/plugins/security/public/management/role_table_display/role_table_display.tsx index 28978f0090011..c1349eba9cddc 100644 --- a/x-pack/plugins/security/public/management/role_table_display/role_table_display.tsx +++ b/x-pack/plugins/security/public/management/role_table_display/role_table_display.tsx @@ -6,19 +6,20 @@ import React from 'react'; import { EuiLink, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; import { Role, isRoleDeprecated, getExtendedRoleDeprecationNotice } from '../../../common/model'; -import { getEditRoleHref } from '../management_urls'; interface Props { role: Role | string; + navigateToApp: ApplicationStart['navigateToApp']; } -export const RoleTableDisplay = ({ role }: Props) => { +export const RoleTableDisplay = ({ role, navigateToApp }: Props) => { let content; - let href; + let path: string; if (typeof role === 'string') { content =
{role}
; - href = getEditRoleHref(role); + path = `security/roles/edit/${encodeURIComponent(role)}`; } else if (isRoleDeprecated(role)) { content = ( {
); - href = getEditRoleHref(role.name); + path = `security/roles/edit/${encodeURIComponent(role.name)}`; } else { content =
{role.name}
; - href = getEditRoleHref(role.name); + path = `security/roles/edit/${encodeURIComponent(role.name)}`; } - return {content}; + + return navigateToApp('management', { path })}>{content}; }; diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts index 98110a83103aa..6821c163d817d 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts @@ -11,13 +11,15 @@ import { Feature } from '../../../../../features/public'; import { KibanaPrivileges } from '../model'; import { SecurityLicenseFeatures } from '../../..'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { featuresPluginMock } from '../../../../../features/server/mocks'; + export const createRawKibanaPrivileges = ( features: Feature[], { allowSubFeaturePrivileges = true } = {} ) => { - const featuresService = { - getFeatures: () => features, - }; + const featuresService = featuresPluginMock.createSetup(); + featuresService.getFeatures.mockReturnValue(features); const licensingService = { getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures), diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index f1ee681331005..43387d913e6fc 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -8,7 +8,7 @@ import { ReactWrapper } from 'enzyme'; import React from 'react'; import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { Capabilities } from 'src/core/public'; +import { Capabilities, ScopedHistory } from 'src/core/public'; import { Feature } from '../../../../../features/public'; import { Role } from '../../../../common/model'; import { DocumentationLinksService } from '../documentation_links'; @@ -16,7 +16,7 @@ import { EditRolePage } from './edit_role_page'; import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; import { TransformErrorSection } from './privileges/kibana/transform_error_section'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { licenseMock } from '../../../../common/licensing/index.mock'; import { userAPIClientMock } from '../../users/index.mock'; @@ -163,7 +163,12 @@ function getProps({ const { http, docLinks, notifications } = coreMock.createStart(); http.get.mockImplementation(async (path: any) => { if (path === '/api/spaces/space') { - return buildSpaces(); + if (spacesEnabled) { + return buildSpaces(); + } + + const notFoundError = { response: { status: 404 } }; + throw notFoundError; } }); @@ -181,8 +186,8 @@ function getProps({ notifications, docLinks: new DocumentationLinksService(docLinks), fatalErrors, - spacesEnabled, uiCapabilities: buildUICapabilities(canManageSpaces), + history: (scopedHistoryMock.create() as unknown) as ScopedHistory, }; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 3688f31be219b..15888733ec424 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -26,6 +26,7 @@ import React, { Fragment, FunctionComponent, HTMLProps, + useCallback, useEffect, useRef, useState, @@ -37,6 +38,7 @@ import { IHttpFetchError, NotificationsStart, } from 'src/core/public'; +import { ScopedHistory } from 'kibana/public'; import { FeaturesPluginStart } from '../../../../../features/public'; import { Feature } from '../../../../../features/common'; import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; @@ -53,7 +55,6 @@ import { RoleIndexPrivilege, getExtendedRoleDeprecationNotice, } from '../../../../common/model'; -import { ROLES_PATH } from '../../management_urls'; import { RoleValidationResult, RoleValidator } from './validate_role'; import { DeleteRoleButton } from './delete_role_button'; import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; @@ -65,6 +66,7 @@ import { IndicesAPIClient } from '../indices_api_client'; import { RolesAPIClient } from '../roles_api_client'; import { PrivilegesAPIClient } from '../privileges_api_client'; import { KibanaPrivileges } from '../model'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { action: 'edit' | 'clone'; @@ -78,10 +80,10 @@ interface Props { docLinks: DocumentationLinksService; http: HttpStart; license: SecurityLicense; - spacesEnabled: boolean; uiCapabilities: Capabilities; notifications: NotificationsStart; fatalErrors: FatalErrorsSetup; + history: ScopedHistory; } function useRunAsUsers( @@ -156,6 +158,7 @@ function useRole( notifications: NotificationsStart, license: SecurityLicense, action: string, + backToRoleList: () => void, roleName?: string ) { const [role, setRole] = useState(null); @@ -216,19 +219,26 @@ function useRole( fatalErrors.add(err); } }); - }, [roleName, action, fatalErrors, rolesAPIClient, notifications, license]); + }, [roleName, action, fatalErrors, rolesAPIClient, notifications, license, backToRoleList]); return [role, setRole] as [Role | null, typeof setRole]; } -function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled: boolean) { - const [spaces, setSpaces] = useState(null); +function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup) { + const [spaces, setSpaces] = useState<{ enabled: boolean; list: Space[] } | null>(null); useEffect(() => { - (spacesEnabled ? http.get('/api/spaces/space') : Promise.resolve([])).then( - (fetchedSpaces) => setSpaces(fetchedSpaces), - (err) => fatalErrors.add(err) + http.get('/api/spaces/space').then( + (fetchedSpaces) => setSpaces({ enabled: true, list: fetchedSpaces }), + (err: IHttpFetchError) => { + // Spaces plugin can be disabled and hence this endpoint can be unavailable. + if (err.response?.status === 404) { + setSpaces({ enabled: false, list: [] }); + } else { + fatalErrors.add(err); + } + } ); - }, [http, fatalErrors, spacesEnabled]); + }, [http, fatalErrors]); return spaces; } @@ -263,10 +273,6 @@ function useFeatures( return features; } -function backToRoleList() { - window.location.hash = ROLES_PATH; -} - export const EditRolePage: FunctionComponent = ({ userAPIClient, indexPatterns, @@ -278,12 +284,14 @@ export const EditRolePage: FunctionComponent = ({ roleName, action, fatalErrors, - spacesEnabled, license, docLinks, uiCapabilities, notifications, + history, }) => { + const backToRoleList = useCallback(() => history.push('/'), [history]); + // We should keep the same mutable instance of Validator for every re-render since we'll // eventually enable validation after the first time user tries to save a role. const { current: validator } = useRef(new RoleValidator({ shouldValidate: false })); @@ -292,7 +300,7 @@ export const EditRolePage: FunctionComponent = ({ const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); const privileges = usePrivileges(privilegesAPIClient, fatalErrors); - const spaces = useSpaces(http, fatalErrors, spacesEnabled); + const spaces = useSpaces(http, fatalErrors); const features = useFeatures(getFeatures, fatalErrors); const [role, setRole] = useRole( rolesAPIClient, @@ -300,6 +308,7 @@ export const EditRolePage: FunctionComponent = ({ notifications, license, action, + backToRoleList, roleName ); @@ -430,8 +439,8 @@ export const EditRolePage: FunctionComponent = ({ = ({ const getReturnToRoleListButton = () => { return ( - + = ({ setFormError(null); try { - await rolesAPIClient.saveRole({ role, spacesEnabled }); + await rolesAPIClient.saveRole({ role, spacesEnabled: spaces.enabled }); } catch (error) { notifications.toasts.addDanger(get(error, 'data.message')); return; @@ -550,7 +559,7 @@ export const EditRolePage: FunctionComponent = ({ backToRoleList(); }; - const description = spacesEnabled ? ( + const description = spaces.enabled ? ( = ({
{getFormTitle()} - - {description} - {isRoleReserved && ( @@ -584,7 +590,6 @@ export const EditRolePage: FunctionComponent = ({ )} - {isDeprecatedRole && ( @@ -595,17 +600,11 @@ export const EditRolePage: FunctionComponent = ({ /> )} - - {getRoleName()} - {getElasticsearchPrivileges()} - {getKibanaPrivileges()} - - {getFormButtons()}
diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index e0a7d96d1cf72..743510d45107e 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -12,10 +12,11 @@ import { RolesAPIClient } from '../roles_api_client'; import { PermissionDenied } from './permission_denied'; import { RolesGridPage } from './roles_grid_page'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { rolesAPIClientMock } from '../index.mock'; import { ReservedBadge, DisabledBadge } from '../../badges'; import { findTestSubject } from 'test_utils/find_test_subject'; +import { ScopedHistory } from 'kibana/public'; const mock403 = () => ({ body: { statusCode: 403 } }); @@ -41,7 +42,10 @@ const waitForRender = async ( describe('', () => { let apiClientMock: jest.Mocked>; + let history: ScopedHistory; + beforeEach(() => { + history = (scopedHistoryMock.create() as unknown) as ScopedHistory; apiClientMock = rolesAPIClientMock.create(); apiClientMock.getRoles.mockResolvedValue([ { @@ -68,6 +72,7 @@ describe('', () => { const wrapper = mountWithIntl( ); @@ -85,6 +90,7 @@ describe('', () => { const wrapper = mountWithIntl( ); @@ -104,6 +110,7 @@ describe('', () => { const wrapper = mountWithIntl( ); @@ -117,6 +124,7 @@ describe('', () => { const wrapper = mountWithIntl( ); @@ -146,6 +154,7 @@ describe('', () => { const wrapper = mountWithIntl( ); diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 4f0d7ca8621a3..051c16f03d342 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -26,6 +26,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; +import { ScopedHistory } from 'kibana/public'; import { Role, isRoleEnabled, @@ -38,10 +39,12 @@ import { RolesAPIClient } from '../roles_api_client'; import { ConfirmDelete } from './confirm_delete'; import { PermissionDenied } from './permission_denied'; import { DisabledBadge, DeprecatedBadge, ReservedBadge } from '../../badges'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { notifications: NotificationsStart; rolesAPIClient: PublicMethodsOf; + history: ScopedHistory; } interface State { @@ -55,7 +58,7 @@ interface State { } const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { - return `#/management/security/roles/${action}${roleName ? `/${roleName}` : ''}`; + return `/${action}${roleName ? `/${roleName}` : ''}`; }; export class RolesGridPage extends Component { @@ -106,7 +109,10 @@ export class RolesGridPage extends Component {
- + { render: (name: string, record: Role) => { return ( - + {name} @@ -228,7 +237,10 @@ export class RolesGridPage extends Component { title={title} color={'primary'} iconType={'pencil'} - href={getRoleManagementHref('edit', role.name)} + {...reactRouterNavigate( + this.props.history, + getRoleManagementHref('edit', role.name) + )} /> ); }, @@ -248,7 +260,10 @@ export class RolesGridPage extends Component { title={title} color={'primary'} iconType={'copy'} - href={getRoleManagementHref('clone', role.name)} + {...reactRouterNavigate( + this.props.history, + getRoleManagementHref('edit', role.name) + )} /> ); }, 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 96051dbd7fa56..e7f38c86b045e 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 @@ -14,12 +14,14 @@ jest.mock('./edit_role', () => ({ EditRolePage: (props: any) => `Role Edit Page: ${JSON.stringify(props)}`, })); +import { ScopedHistory } from 'src/core/public'; + import { rolesManagementApp } from './roles_management_app'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; import { featuresPluginMock } from '../../../../features/public/mocks'; -async function mountApp(basePath: string) { +async function mountApp(basePath: string, pathname: string) { const { fatalErrors } = coreMock.createSetup(); const container = document.createElement('div'); const setBreadcrumbs = jest.fn(); @@ -34,7 +36,12 @@ async function mountApp(basePath: string) { .fn() .mockResolvedValue([coreMock.createStart(), { data: {}, features: featuresStart }]), }) - .mount({ basePath, element: container, setBreadcrumbs }); + .mount({ + basePath, + element: container, + setBreadcrumbs, + history: (scopedHistoryMock.create({ pathname }) as unknown) as ScopedHistory, + }); return { unmount, container, setBreadcrumbs }; } @@ -60,16 +67,13 @@ describe('rolesManagementApp', () => { }); it('mount() works for the `grid` page', async () => { - const basePath = '/some-base-path/roles'; - window.location.hash = basePath; - - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', '/'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Roles' }]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }]); expect(container).toMatchInlineSnapshot(`
- Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}}} + Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}}
`); @@ -79,19 +83,13 @@ describe('rolesManagementApp', () => { }); it('mount() works for the `create role` page', async () => { - const basePath = '/some-base-path/roles'; - window.location.hash = `${basePath}/edit`; - - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', '/edit'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Roles' }, - { text: 'Create' }, - ]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }, { text: 'Create' }]); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","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":{}}} + Role Edit Page: {"action":"edit","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","search":"","hash":""}}}
`); @@ -101,20 +99,18 @@ describe('rolesManagementApp', () => { }); it('mount() works for the `edit role` page', async () => { - const basePath = '/some-base-path/roles'; const roleName = 'someRoleName'; - window.location.hash = `${basePath}/edit/${roleName}`; - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${roleName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Roles' }, - { href: `#/some-base-path/roles/edit/${roleName}`, text: roleName }, + { href: `/`, text: 'Roles' }, + { href: `/edit/${roleName}`, text: roleName }, ]); expect(container).toMatchInlineSnapshot(`
- 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":{}}} + 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":""}}}
`); @@ -124,20 +120,15 @@ describe('rolesManagementApp', () => { }); it('mount() works for the `clone role` page', async () => { - const basePath = '/some-base-path/roles'; const roleName = 'someRoleName'; - window.location.hash = `${basePath}/clone/${roleName}`; - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', `/clone/${roleName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Roles' }, - { text: 'Create' }, - ]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }, { text: 'Create' }]); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"clone","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":{}}} + Role Edit Page: {"action":"clone","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":"/clone/someRoleName","search":"","hash":""}}}
`); @@ -147,17 +138,15 @@ describe('rolesManagementApp', () => { }); it('mount() properly encodes role name in `edit role` page link in breadcrumbs', async () => { - const basePath = '/some-base-path/roles'; const roleName = 'some 安全性 role'; - window.location.hash = `${basePath}/edit/${roleName}`; - const { setBreadcrumbs } = await mountApp(basePath); + const { setBreadcrumbs } = await mountApp('/', `/edit/${roleName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Roles' }, + { href: `/`, text: 'Roles' }, { - href: '#/some-base-path/roles/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role', + href: '/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role', text: roleName, }, ]); 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 9aaa3b47f3b19..88aeb1d232fc7 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 @@ -6,7 +6,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { Router, Route, Switch, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; @@ -27,19 +27,16 @@ export const rolesManagementApp = Object.freeze({ id: this.id, order: 20, title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), - async mount({ basePath, element, setBreadcrumbs }) { + async mount({ element, setBreadcrumbs, history }) { const rolesBreadcrumbs = [ { text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), - href: `#${basePath}`, + href: `/`, }, ]; const [ - [ - { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, - { data, features }, - ], + [{ application, docLinks, http, i18n: i18nStart, notifications }, { data, features }], { RolesGridPage }, { EditRolePage }, { RolesAPIClient }, @@ -59,7 +56,13 @@ export const rolesManagementApp = Object.freeze({ const rolesAPIClient = new RolesAPIClient(http); const RolesGridPageWithBreadcrumbs = () => { setBreadcrumbs(rolesBreadcrumbs); - return ; + return ( + + ); }; const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { @@ -68,7 +71,7 @@ export const rolesManagementApp = Object.freeze({ setBreadcrumbs([ ...rolesBreadcrumbs, action === 'edit' && roleName - ? { text: roleName, href: `#${basePath}/edit/${encodeURIComponent(roleName)}` } + ? { text: roleName, href: `/edit/${encodeURIComponent(roleName)}` } : { text: i18n.translate('xpack.security.roles.createBreadcrumb', { defaultMessage: 'Create', @@ -80,9 +83,6 @@ export const rolesManagementApp = Object.freeze({ ); }; render( - + - + diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index a97781ba25ea6..7ee33357b9af4 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -5,12 +5,13 @@ */ import { act } from '@testing-library/react'; +import { ScopedHistory } from 'kibana/public'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { EditUserPage } from './edit_user_page'; import React from 'react'; import { User, Role } from '../../../../common/model'; import { ReactWrapper } from 'enzyme'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; import { securityMock } from '../../../mocks'; import { rolesAPIClientMock } from '../../roles/index.mock'; @@ -103,6 +104,8 @@ function expectMissingSaveButton(wrapper: ReactWrapper) { } describe('EditUserPage', () => { + const history = (scopedHistoryMock.create() as unknown) as ScopedHistory; + it('allows reserved users to be viewed', async () => { const user = createUser('reserved_user'); const { apiClient, rolesAPIClient } = buildClients(user); @@ -114,6 +117,7 @@ describe('EditUserPage', () => { rolesAPIClient={rolesAPIClient} authc={securitySetup.authc} notifications={coreMock.createStart().notifications} + history={history} /> ); @@ -136,6 +140,7 @@ describe('EditUserPage', () => { rolesAPIClient={rolesAPIClient} authc={securitySetup.authc} notifications={coreMock.createStart().notifications} + history={history} /> ); @@ -158,6 +163,7 @@ describe('EditUserPage', () => { rolesAPIClient={rolesAPIClient} authc={securitySetup.authc} notifications={coreMock.createStart().notifications} + history={history} /> ); @@ -182,6 +188,7 @@ describe('EditUserPage', () => { rolesAPIClient={rolesAPIClient} authc={securitySetup.authc} notifications={coreMock.createStart().notifications} + history={history} /> ); @@ -204,6 +211,7 @@ describe('EditUserPage', () => { rolesAPIClient={rolesAPIClient} authc={securitySetup.authc} notifications={coreMock.createStart().notifications} + history={history} /> ); diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 49da4c66a7630..eea7edd62fbfa 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -30,10 +30,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NotificationsStart } from 'src/core/public'; +import { NotificationsStart, ScopedHistory } from 'src/core/public'; import { User, EditUser, Role, isRoleDeprecated } from '../../../../common/model'; import { AuthenticationServiceSetup } from '../../../authentication'; -import { USERS_PATH } from '../../management_urls'; import { RolesAPIClient } from '../../roles'; import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; import { UserValidator, UserValidationResult } from './validate_user'; @@ -47,6 +46,7 @@ interface Props { rolesAPIClient: PublicMethodsOf; authc: AuthenticationServiceSetup; notifications: NotificationsStart; + history: ScopedHistory; } interface State { @@ -61,10 +61,6 @@ interface State { formError: UserValidationResult | null; } -function backToUserList() { - window.location.hash = USERS_PATH; -} - export class EditUserPage extends Component { private validator: UserValidator; @@ -102,6 +98,10 @@ export class EditUserPage extends Component { } } + private backToUserList() { + this.props.history.push('/'); + } + private async setCurrentUser() { const { username, userAPIClient, rolesAPIClient, notifications, authc } = this.props; let { user, currentUser } = this.state; @@ -120,7 +120,7 @@ export class EditUserPage extends Component { }), text: get(err, 'body.message') || err.message, }); - return backToUserList(); + return this.backToUserList(); } } @@ -148,7 +148,7 @@ export class EditUserPage extends Component { private handleDelete = (usernames: string[], errors: string[]) => { if (errors.length === 0) { - backToUserList(); + this.backToUserList(); } }; @@ -184,7 +184,7 @@ export class EditUserPage extends Component { ) ); - backToUserList(); + this.backToUserList(); } catch (e) { this.props.notifications.toasts.addDanger( i18n.translate('xpack.security.management.users.editUser.savingUserErrorMessage', { @@ -549,7 +549,7 @@ export class EditUserPage extends Component { {reserved && ( - + this.backToUserList()}> {
- + this.backToUserList()} + > { + let history: ScopedHistory; + let coreStart: CoreStart; + + beforeEach(() => { + history = (scopedHistoryMock.create() as unknown) as ScopedHistory; + history.createHref = (location: LocationDescriptorObject) => { + return `${location.pathname}${location.search ? '?' + location.search : ''}`; + }; + coreStart = coreMock.createStart(); + }); + it('renders the list of users', async () => { const apiClientMock = userAPIClientMock.create(); apiClientMock.getUsers.mockImplementation(() => { @@ -44,7 +57,9 @@ describe('UsersGridPage', () => { ); @@ -64,7 +79,9 @@ describe('UsersGridPage', () => { ); @@ -93,7 +110,9 @@ describe('UsersGridPage', () => { ); @@ -125,7 +144,9 @@ describe('UsersGridPage', () => { ); @@ -173,7 +194,9 @@ describe('UsersGridPage', () => { ); @@ -231,7 +254,9 @@ describe('UsersGridPage', () => { ); diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index 5fd2b4ec85589..50815808c4859 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -23,19 +23,22 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NotificationsStart } from 'src/core/public'; +import { NotificationsStart, ApplicationStart, ScopedHistory } from 'src/core/public'; import { User, Role } from '../../../../common/model'; import { ConfirmDeleteUsers } from '../components'; import { isUserReserved, getExtendedUserDeprecationNotice, isUserDeprecated } from '../user_utils'; import { DisabledBadge, ReservedBadge, DeprecatedBadge } from '../../badges'; import { RoleTableDisplay } from '../../role_table_display'; import { RolesAPIClient } from '../../roles'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { UserAPIClient } from '..'; interface Props { userAPIClient: PublicMethodsOf; rolesAPIClient: PublicMethodsOf; notifications: NotificationsStart; + history: ScopedHistory; + navigateToApp: ApplicationStart['navigateToApp']; } interface State { @@ -70,6 +73,7 @@ export class UsersGridPage extends Component { public render() { const { users, roles, permissionDenied, showDeleteConfirmation, selection } = this.state; + if (permissionDenied) { return ( @@ -97,7 +101,6 @@ export class UsersGridPage extends Component { ); } - const path = '#/management/security/'; const columns: Array> = [ { field: 'username', @@ -107,7 +110,10 @@ export class UsersGridPage extends Component { sortable: true, truncateText: true, render: (username: string) => ( - + {username} ), @@ -144,7 +150,13 @@ export class UsersGridPage extends Component { render: (rolenames: string[]) => { const roleLinks = rolenames.map((rolename, index) => { const roleDefinition = roles?.find((role) => role.name === rolename) ?? rolename; - return ; + return ( + + ); }); return
{roleLinks}
; }, @@ -219,7 +231,10 @@ export class UsersGridPage extends Component { - + ({ EditUserPage: (props: any) => `User Edit Page: ${JSON.stringify(props)}`, })); +import { ScopedHistory } from 'src/core/public'; import { usersManagementApp } from './users_management_app'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; import { securityMock } from '../../mocks'; -async function mountApp(basePath: string) { +async function mountApp(basePath: string, pathname: string) { const container = document.createElement('div'); const setBreadcrumbs = jest.fn(); @@ -26,7 +27,12 @@ async function mountApp(basePath: string) { authc: securityMock.createSetup().authc, getStartServices: coreMock.createSetup().getStartServices as any, }) - .mount({ basePath, element: container, setBreadcrumbs }); + .mount({ + basePath, + element: container, + setBreadcrumbs, + history: (scopedHistoryMock.create({ pathname }) as unknown) as ScopedHistory, + }); return { unmount, container, setBreadcrumbs }; } @@ -49,16 +55,13 @@ describe('usersManagementApp', () => { }); it('mount() works for the `grid` page', async () => { - const basePath = '/some-base-path/users'; - window.location.hash = basePath; - - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', '/'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Users' }]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }]); expect(container).toMatchInlineSnapshot(`
- Users Page: {"notifications":{"toasts":{}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}}} + Users Page: {"notifications":{"toasts":{}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}}
`); @@ -68,19 +71,13 @@ describe('usersManagementApp', () => { }); it('mount() works for the `create user` page', async () => { - const basePath = '/some-base-path/users'; - window.location.hash = `${basePath}/edit`; - - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', '/edit'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Users' }, - { text: 'Create' }, - ]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }, { text: 'Create' }]); expect(container).toMatchInlineSnapshot(`
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}} + User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
`); @@ -90,20 +87,18 @@ describe('usersManagementApp', () => { }); it('mount() works for the `edit user` page', async () => { - const basePath = '/some-base-path/users'; const userName = 'someUserName'; - window.location.hash = `${basePath}/edit/${userName}`; - const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${userName}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Users' }, - { href: `#/some-base-path/users/edit/${userName}`, text: userName }, + { href: `/`, text: 'Users' }, + { href: `/edit/${userName}`, text: userName }, ]); expect(container).toMatchInlineSnapshot(`
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"} + 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":""}}}
`); @@ -113,17 +108,15 @@ describe('usersManagementApp', () => { }); it('mount() properly encodes user name in `edit user` page link in breadcrumbs', async () => { - const basePath = '/some-base-path/users'; const username = 'some 安全性 user'; - window.location.hash = `${basePath}/edit/${username}`; - const { setBreadcrumbs } = await mountApp(basePath); + const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `#${basePath}`, text: 'Users' }, + { href: `/`, text: 'Users' }, { - href: '#/some-base-path/users/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', + href: '/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', 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 9d337c1508ad4..82c55d67b9026 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 @@ -6,7 +6,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { Router, Route, Switch, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; @@ -25,11 +25,12 @@ export const usersManagementApp = Object.freeze({ id: this.id, order: 10, title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), - async mount({ basePath, element, setBreadcrumbs }) { + async mount({ element, setBreadcrumbs, history }) { + const [coreStart] = await getStartServices(); const usersBreadcrumbs = [ { text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), - href: `#${basePath}`, + href: `/`, }, ]; @@ -56,6 +57,8 @@ export const usersManagementApp = Object.freeze({ notifications={notifications} userAPIClient={userAPIClient} rolesAPIClient={rolesAPIClient} + history={history} + navigateToApp={coreStart.application.navigateToApp} /> ); }; @@ -66,7 +69,7 @@ export const usersManagementApp = Object.freeze({ setBreadcrumbs([ ...usersBreadcrumbs, username - ? { text: username, href: `#${basePath}/edit/${encodeURIComponent(username)}` } + ? { text: username, href: `/edit/${encodeURIComponent(username)}` } : { text: i18n.translate('xpack.security.users.createBreadcrumb', { defaultMessage: 'Create', @@ -81,15 +84,16 @@ export const usersManagementApp = Object.freeze({ rolesAPIClient={new RolesAPIClient(http)} notifications={notifications} username={username} + history={history} /> ); }; render( - + - + diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 38ef552e75a9e..da69dd051c11d 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -122,7 +122,7 @@ export class SecurityPlugin 'Protect your data and easily manage who has access to what with users and roles.', }), icon: 'securityApp', - path: '/app/kibana#/management/security/users', + path: '/app/management/security/users', showOnHomePage: true, category: FeatureCatalogueCategory.ADMIN, }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 49b7b40659cfc..60d0521a2947e 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -29,6 +29,7 @@ import { AuthenticationResult } from './authentication_result'; import { Authenticator, AuthenticatorOptions, ProviderSession } from './authenticator'; import { DeauthenticationResult } from './deauthentication_result'; import { BasicAuthenticationProvider, SAMLAuthenticationProvider } from './providers'; +import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock'; function getMockOptions({ session, @@ -54,6 +55,9 @@ function getMockOptions({ { isTLSEnabled: false } ), sessionStorageFactory: sessionStorageMock.createFactory(), + getFeatureUsageService: jest + .fn() + .mockReturnValue(securityFeatureUsageServiceMock.createStartContract()), }; } @@ -1451,6 +1455,9 @@ describe('Authenticator', () => { ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).not.toHaveBeenCalled(); }); it('fails if cannot retrieve user session', async () => { @@ -1463,6 +1470,9 @@ describe('Authenticator', () => { ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).not.toHaveBeenCalled(); }); it('fails if license doesn allow access agreement acknowledgement', async () => { @@ -1477,6 +1487,9 @@ describe('Authenticator', () => { ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).not.toHaveBeenCalled(); }); it('properly acknowledges access agreement for the authenticated user', async () => { @@ -1493,6 +1506,10 @@ describe('Authenticator', () => { type: 'basic', name: 'basic1', }); + + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 98342a8494e38..ac5c2a72b9667 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -38,6 +38,7 @@ import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; import { canRedirectRequest } from './can_redirect_request'; import { HTTPAuthorizationHeader } from './http_authentication'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; /** * The shape of the session that is actually stored in the cookie. @@ -94,6 +95,7 @@ export interface ProviderLoginAttempt { export interface AuthenticatorOptions { auditLogger: SecurityAuditLogger; + getFeatureUsageService: () => SecurityFeatureUsageServiceStart; getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; config: Pick; basePath: HttpServiceSetup['basePath']; @@ -502,6 +504,8 @@ export class Authenticator { currentUser.username, existingSession.provider ); + + this.options.getFeatureUsageService().recordPreAccessAgreementUsage(); } /** diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 1c1e0ed781f18..c7323509c00d6 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -42,6 +42,8 @@ import { } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; import { SecurityAuditLogger } from '../audit'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; +import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock'; describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { @@ -51,6 +53,7 @@ describe('setupAuthentication()', () => { http: jest.Mocked; clusterClient: jest.Mocked; license: jest.Mocked; + getFeatureUsageService: () => jest.Mocked; }; let mockScopedClusterClient: jest.Mocked>; beforeEach(() => { @@ -69,6 +72,9 @@ describe('setupAuthentication()', () => { clusterClient: elasticsearchServiceMock.createClusterClient(), license: licenseMock.create(), loggers: loggingServiceMock.create(), + getFeatureUsageService: jest + .fn() + .mockReturnValue(securityFeatureUsageServiceMock.createStartContract()), }; mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 779b852195b02..ec48c727a5739 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -17,6 +17,7 @@ import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { Authenticator, ProviderSession } from './authenticator'; import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; export { canRedirectRequest } from './can_redirect_request'; export { Authenticator, ProviderLoginAttempt } from './authenticator'; @@ -37,6 +38,7 @@ export { interface SetupAuthenticationParams { auditLogger: SecurityAuditLogger; + getFeatureUsageService: () => SecurityFeatureUsageServiceStart; http: CoreSetup['http']; clusterClient: IClusterClient; config: ConfigType; @@ -48,6 +50,7 @@ export type Authentication = UnwrapPromise { diff --git a/x-pack/plugins/security/server/authorization/app_authorization.ts b/x-pack/plugins/security/server/authorization/app_authorization.ts index aead8cb07897c..1036997ca821d 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, Logger } from '../../../../../src/core/server'; -import { FeaturesService } from '../plugin'; -import { Authorization } from '.'; +import { HttpServiceSetup, Logger } from '../../../../../src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../features/server'; +import { AuthorizationServiceSetup } from '.'; class ProtectedApplications { private applications: Set | null = null; - constructor(private readonly featuresService: FeaturesService) {} + constructor(private readonly featuresService: FeaturesPluginSetup) {} public shouldProtect(appId: string) { // Currently, once we get the list of features we essentially "lock" additional @@ -30,14 +30,14 @@ class ProtectedApplications { } export function initAppAuthorization( - http: CoreSetup['http'], + http: HttpServiceSetup, { actions, checkPrivilegesDynamicallyWithRequest, mode, - }: Pick, + }: Pick, logger: Logger, - featuresService: FeaturesService + featuresService: FeaturesPluginSetup ) { const protectedApplications = new ProtectedApplications(featuresService); diff --git a/x-pack/plugins/security/server/authorization/authorization_service.test.ts b/x-pack/plugins/security/server/authorization/authorization_service.test.ts new file mode 100644 index 0000000000000..978c985cfe820 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -0,0 +1,263 @@ +/* + * 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 { + mockAuthorizationModeFactory, + mockCheckPrivilegesDynamicallyWithRequestFactory, + mockCheckPrivilegesWithRequestFactory, + mockCheckSavedObjectsPrivilegesWithRequestFactory, + mockPrivilegesFactory, + mockRegisterPrivilegesWithCluster, +} from './service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import { CoreStatus, ServiceStatusLevels } from '../../../../../src/core/server'; +import { checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; +import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; +import { authorizationModeFactory } from './mode'; +import { privilegesFactory } from './privileges'; +import { AuthorizationService } from '.'; + +import { + coreMock, + elasticsearchServiceMock, + loggingServiceMock, +} from '../../../../../src/core/server/mocks'; +import { featuresPluginMock } from '../../../features/server/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../common/licensing'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +const kibanaIndexName = '.a-kibana-index'; +const application = `kibana-${kibanaIndexName}`; +const mockCheckPrivilegesWithRequest = Symbol(); +const mockCheckPrivilegesDynamicallyWithRequest = Symbol(); +const mockCheckSavedObjectsPrivilegesWithRequest = Symbol(); +const mockPrivilegesService = Symbol(); +const mockAuthorizationMode = Symbol(); +beforeEach(() => { + mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest); + mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue( + mockCheckPrivilegesDynamicallyWithRequest + ); + mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue( + mockCheckSavedObjectsPrivilegesWithRequest + ); + mockPrivilegesFactory.mockReturnValue(mockPrivilegesService); + mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode); +}); + +afterEach(() => { + mockRegisterPrivilegesWithCluster.mockClear(); +}); + +it(`#setup returns exposed services`, () => { + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockGetSpacesService = jest + .fn() + .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); + const mockFeaturesSetup = featuresPluginMock.createSetup(); + const mockLicense = licenseMock.create(); + const mockCoreSetup = coreMock.createSetup(); + + const authorizationService = new AuthorizationService(); + const authz = authorizationService.setup({ + http: mockCoreSetup.http, + capabilities: mockCoreSetup.capabilities, + status: mockCoreSetup.status, + clusterClient: mockClusterClient, + license: mockLicense, + loggers: loggingServiceMock.create(), + kibanaIndexName, + packageVersion: 'some-version', + features: mockFeaturesSetup, + getSpacesService: mockGetSpacesService, + }); + + expect(authz.actions.version).toBe('version:some-version'); + expect(authz.applicationName).toBe(application); + + expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest); + expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( + authz.actions, + mockClusterClient, + authz.applicationName + ); + + expect(authz.checkPrivilegesDynamicallyWithRequest).toBe( + mockCheckPrivilegesDynamicallyWithRequest + ); + expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith( + mockCheckPrivilegesWithRequest, + mockGetSpacesService + ); + + expect(authz.checkSavedObjectsPrivilegesWithRequest).toBe( + mockCheckSavedObjectsPrivilegesWithRequest + ); + expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith( + mockCheckPrivilegesWithRequest, + mockGetSpacesService + ); + + expect(authz.privileges).toBe(mockPrivilegesService); + expect(privilegesFactory).toHaveBeenCalledWith(authz.actions, mockFeaturesSetup, mockLicense); + + expect(authz.mode).toBe(mockAuthorizationMode); + expect(authorizationModeFactory).toHaveBeenCalledWith(mockLicense); + + expect(mockCoreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + expect(mockCoreSetup.capabilities.registerSwitcher).toHaveBeenCalledWith(expect.any(Function)); +}); + +describe('#start', () => { + let statusSubject: BehaviorSubject; + let licenseSubject: BehaviorSubject; + let mockLicense: jest.Mocked; + beforeEach(() => { + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + + licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures); + mockLicense = licenseMock.create(); + mockLicense.isEnabled.mockReturnValue(false); + mockLicense.features$ = licenseSubject; + + statusSubject = new BehaviorSubject({ + elasticsearch: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + }); + const mockCoreSetup = coreMock.createSetup(); + mockCoreSetup.status.core$ = statusSubject; + + const authorizationService = new AuthorizationService(); + authorizationService.setup({ + http: mockCoreSetup.http, + capabilities: mockCoreSetup.capabilities, + status: mockCoreSetup.status, + clusterClient: mockClusterClient, + license: mockLicense, + loggers: loggingServiceMock.create(), + kibanaIndexName, + packageVersion: 'some-version', + features: featuresPluginMock.createSetup(), + getSpacesService: jest + .fn() + .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + }); + + const featuresStart = featuresPluginMock.createStart(); + featuresStart.getFeatures.mockReturnValue([]); + + authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart }); + + // ES and license aren't available yet. + expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled(); + }); + + it('registers cluster privileges', async () => { + // ES is available now, but not license. + statusSubject.next({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + }); + expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled(); + + // Both ES and license are available now. + mockLicense.isEnabled.mockReturnValue(true); + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1); + + await nextTick(); + + // New changes still trigger privileges re-registration. + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2); + }); + + it('schedules retries if fails to register cluster privileges', async () => { + jest.useFakeTimers(); + + mockRegisterPrivilegesWithCluster.mockRejectedValue(new Error('Some error')); + + // Both ES and license are available. + mockLicense.isEnabled.mockReturnValue(true); + statusSubject.next({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + }); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1); + + // Next retry isn't performed immediately, retry happens only after a timeout. + await nextTick(); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(100); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2); + + // Delay between consequent retries is increasing. + await nextTick(); + jest.advanceTimersByTime(100); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2); + await nextTick(); + jest.advanceTimersByTime(100); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(3); + + // When call finally succeeds retries aren't scheduled anymore. + mockRegisterPrivilegesWithCluster.mockResolvedValue(undefined); + await nextTick(); + jest.runAllTimers(); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(4); + await nextTick(); + jest.runAllTimers(); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(4); + + // New changes still trigger privileges re-registration. + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(5); + }); +}); + +it('#stop unsubscribes from license and ES updates.', () => { + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + + const licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures); + const mockLicense = licenseMock.create(); + mockLicense.isEnabled.mockReturnValue(false); + mockLicense.features$ = licenseSubject; + + const mockCoreSetup = coreMock.createSetup(); + mockCoreSetup.status.core$ = new BehaviorSubject({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + savedObjects: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + }); + + const authorizationService = new AuthorizationService(); + authorizationService.setup({ + http: mockCoreSetup.http, + capabilities: mockCoreSetup.capabilities, + status: mockCoreSetup.status, + clusterClient: mockClusterClient, + license: mockLicense, + loggers: loggingServiceMock.create(), + kibanaIndexName, + packageVersion: 'some-version', + features: featuresPluginMock.createSetup(), + getSpacesService: jest + .fn() + .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + }); + + const featuresStart = featuresPluginMock.createStart(); + featuresStart.getFeatures.mockReturnValue([]); + authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart }); + + authorizationService.stop(); + + // After stop we don't register privileges even if all requirements are met. + mockLicense.isEnabled.mockReturnValue(true); + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled(); +}); diff --git a/x-pack/plugins/security/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts new file mode 100644 index 0000000000000..989784a1436d2 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -0,0 +1,221 @@ +/* + * 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 { combineLatest, BehaviorSubject, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter } from 'rxjs/operators'; +import { UICapabilities } from 'ui/capabilities'; +import { + LoggerFactory, + KibanaRequest, + IClusterClient, + ServiceStatusLevels, + Logger, + StatusServiceSetup, + HttpServiceSetup, + CapabilitiesSetup, +} from '../../../../../src/core/server'; + +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../../features/server'; + +import { SpacesService } from '../plugin'; +import { Actions } from './actions'; +import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +import { + CheckPrivilegesDynamicallyWithRequest, + checkPrivilegesDynamicallyWithRequestFactory, +} from './check_privileges_dynamically'; +import { + CheckSavedObjectsPrivilegesWithRequest, + checkSavedObjectsPrivilegesWithRequestFactory, +} from './check_saved_objects_privileges'; +import { AuthorizationMode, authorizationModeFactory } from './mode'; +import { privilegesFactory, PrivilegesService } from './privileges'; +import { initAppAuthorization } from './app_authorization'; +import { initAPIAuthorization } from './api_authorization'; +import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; +import { validateFeaturePrivileges } from './validate_feature_privileges'; +import { validateReservedPrivileges } from './validate_reserved_privileges'; +import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; +import { APPLICATION_PREFIX } from '../../common/constants'; +import { SecurityLicense } from '../../common/licensing'; + +export { Actions } from './actions'; +export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; +export { featurePrivilegeIterator } from './privileges'; + +interface AuthorizationServiceSetupParams { + packageVersion: string; + http: HttpServiceSetup; + status: StatusServiceSetup; + capabilities: CapabilitiesSetup; + clusterClient: IClusterClient; + license: SecurityLicense; + loggers: LoggerFactory; + features: FeaturesPluginSetup; + kibanaIndexName: string; + getSpacesService(): SpacesService | undefined; +} + +interface AuthorizationServiceStartParams { + features: FeaturesPluginStart; + clusterClient: IClusterClient; +} + +export interface AuthorizationServiceSetup { + actions: Actions; + checkPrivilegesWithRequest: CheckPrivilegesWithRequest; + checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; + checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; + applicationName: string; + mode: AuthorizationMode; + privileges: PrivilegesService; +} + +export class AuthorizationService { + private logger!: Logger; + private license!: SecurityLicense; + private status!: StatusServiceSetup; + private applicationName!: string; + private privileges!: PrivilegesService; + + private statusSubscription?: Subscription; + + setup({ + http, + capabilities, + status, + packageVersion, + clusterClient, + license, + loggers, + features, + kibanaIndexName, + getSpacesService, + }: AuthorizationServiceSetupParams): AuthorizationServiceSetup { + this.logger = loggers.get('authorization'); + this.license = license; + this.status = status; + this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; + + const mode = authorizationModeFactory(license); + const actions = new Actions(packageVersion); + this.privileges = privilegesFactory(actions, features, license); + + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + actions, + clusterClient, + this.applicationName + ); + + const authz = { + actions, + applicationName: this.applicationName, + mode, + privileges: this.privileges, + checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory( + checkPrivilegesWithRequest, + getSpacesService + ), + checkSavedObjectsPrivilegesWithRequest: checkSavedObjectsPrivilegesWithRequestFactory( + checkPrivilegesWithRequest, + getSpacesService + ), + }; + + capabilities.registerSwitcher( + async (request: KibanaRequest, uiCapabilities: UICapabilities) => { + // If we have a license which doesn't enable security, or we're a legacy user we shouldn't + // disable any ui capabilities + if (!mode.useRbacForRequest(request)) { + return uiCapabilities; + } + + const disableUICapabilities = disableUICapabilitiesFactory( + request, + features.getFeatures(), + this.logger, + authz + ); + + if (!request.auth.isAuthenticated) { + return disableUICapabilities.all(uiCapabilities); + } + + return await disableUICapabilities.usingPrivileges(uiCapabilities); + } + ); + + initAPIAuthorization(http, authz, loggers.get('api-authorization')); + initAppAuthorization(http, authz, loggers.get('app-authorization'), features); + + return authz; + } + + start({ clusterClient, features }: AuthorizationServiceStartParams) { + const allFeatures = features.getFeatures(); + validateFeaturePrivileges(allFeatures); + validateReservedPrivileges(allFeatures); + + this.registerPrivileges(clusterClient); + } + + stop() { + if (this.statusSubscription !== undefined) { + this.statusSubscription.unsubscribe(); + this.statusSubscription = undefined; + } + } + + private registerPrivileges(clusterClient: IClusterClient) { + const RETRY_SCALE_DURATION = 100; + const RETRY_TIMEOUT_MAX = 10000; + const retries$ = new BehaviorSubject(0); + let retryTimeout: NodeJS.Timeout; + + // Register cluster privileges once Elasticsearch is available and Security plugin is enabled. + this.statusSubscription = combineLatest([ + this.status.core$, + this.license.features$, + retries$.asObservable().pipe( + // We shouldn't emit new value if retry counter is reset. This comparator isn't called for + // the initial value. + distinctUntilChanged((prev, curr) => prev === curr || curr === 0) + ), + ]) + .pipe( + filter( + ([status]) => + this.license.isEnabled() && status.elasticsearch.level === ServiceStatusLevels.available + ) + ) + .subscribe(async () => { + // If status or license change occurred before retry timeout we should cancel it. + if (retryTimeout) { + clearTimeout(retryTimeout); + } + + try { + await registerPrivilegesWithCluster( + this.logger, + this.privileges, + this.applicationName, + clusterClient + ); + retries$.next(0); + } catch (err) { + const retriesElapsed = retries$.getValue() + 1; + retryTimeout = setTimeout( + () => retries$.next(retriesElapsed), + Math.min(retriesElapsed * RETRY_SCALE_DURATION, RETRY_TIMEOUT_MAX) + ); + } + }); + } +} diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 72937c15756ac..183ad9169a123 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -10,13 +10,13 @@ import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { Feature } from '../../../features/server'; import { CheckPrivilegesResponse } from './check_privileges'; -import { Authorization } from './index'; +import { AuthorizationServiceSetup } from '.'; export function disableUICapabilitiesFactory( request: KibanaRequest, features: Feature[], logger: Logger, - authz: Authorization + authz: AuthorizationServiceSetup ) { const featureNavLinkIds = features .map((feature) => feature.navLinkId) diff --git a/x-pack/plugins/security/server/authorization/index.test.ts b/x-pack/plugins/security/server/authorization/index.test.ts deleted file mode 100644 index 3252053454764..0000000000000 --- a/x-pack/plugins/security/server/authorization/index.test.ts +++ /dev/null @@ -1,100 +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 { - mockAuthorizationModeFactory, - mockCheckPrivilegesDynamicallyWithRequestFactory, - mockCheckPrivilegesWithRequestFactory, - mockCheckSavedObjectsPrivilegesWithRequestFactory, - mockPrivilegesFactory, -} from './service.test.mocks'; - -import { checkPrivilegesWithRequestFactory } from './check_privileges'; -import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; -import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; -import { authorizationModeFactory } from './mode'; -import { privilegesFactory } from './privileges'; -import { setupAuthorization } from '.'; - -import { - coreMock, - elasticsearchServiceMock, - loggingServiceMock, -} from '../../../../../src/core/server/mocks'; -import { licenseMock } from '../../common/licensing/index.mock'; - -test(`returns exposed services`, () => { - const kibanaIndexName = '.a-kibana-index'; - const application = `kibana-${kibanaIndexName}`; - - const mockCheckPrivilegesWithRequest = Symbol(); - mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest); - - const mockCheckPrivilegesDynamicallyWithRequest = Symbol(); - mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue( - mockCheckPrivilegesDynamicallyWithRequest - ); - - const mockCheckSavedObjectsPrivilegesWithRequest = Symbol(); - mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue( - mockCheckSavedObjectsPrivilegesWithRequest - ); - - const mockPrivilegesService = Symbol(); - mockPrivilegesFactory.mockReturnValue(mockPrivilegesService); - const mockAuthorizationMode = Symbol(); - mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode); - - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); - const mockGetSpacesService = jest - .fn() - .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); - const mockFeaturesService = { getFeatures: () => [] }; - const mockLicense = licenseMock.create(); - - const authz = setupAuthorization({ - http: coreMock.createSetup().http, - clusterClient: mockClusterClient, - license: mockLicense, - loggers: loggingServiceMock.create(), - kibanaIndexName, - packageVersion: 'some-version', - featuresService: mockFeaturesService, - getSpacesService: mockGetSpacesService, - }); - - expect(authz.actions.version).toBe('version:some-version'); - expect(authz.applicationName).toBe(application); - - expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest); - expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( - authz.actions, - mockClusterClient, - authz.applicationName - ); - - expect(authz.checkPrivilegesDynamicallyWithRequest).toBe( - mockCheckPrivilegesDynamicallyWithRequest - ); - expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith( - mockCheckPrivilegesWithRequest, - mockGetSpacesService - ); - - expect(authz.checkSavedObjectsPrivilegesWithRequest).toBe( - mockCheckSavedObjectsPrivilegesWithRequest - ); - expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith( - mockCheckPrivilegesWithRequest, - mockGetSpacesService - ); - - expect(authz.privileges).toBe(mockPrivilegesService); - expect(privilegesFactory).toHaveBeenCalledWith(authz.actions, mockFeaturesService, mockLicense); - - expect(authz.mode).toBe(mockAuthorizationMode); - expect(authorizationModeFactory).toHaveBeenCalledWith(mockLicense); -}); diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index cf970a561b93f..d5c1323354f86 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -4,134 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UICapabilities } from 'ui/capabilities'; -import { - CoreSetup, - LoggerFactory, - KibanaRequest, - IClusterClient, -} from '../../../../../src/core/server'; - -import { FeaturesService, SpacesService } from '../plugin'; -import { Actions } from './actions'; -import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; -import { - CheckPrivilegesDynamicallyWithRequest, - checkPrivilegesDynamicallyWithRequestFactory, -} from './check_privileges_dynamically'; -import { - CheckSavedObjectsPrivilegesWithRequest, - checkSavedObjectsPrivilegesWithRequestFactory, -} from './check_saved_objects_privileges'; -import { AuthorizationMode, authorizationModeFactory } from './mode'; -import { privilegesFactory, PrivilegesService } from './privileges'; -import { initAppAuthorization } from './app_authorization'; -import { initAPIAuthorization } from './api_authorization'; -import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; -import { validateFeaturePrivileges } from './validate_feature_privileges'; -import { validateReservedPrivileges } from './validate_reserved_privileges'; -import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; -import { APPLICATION_PREFIX } from '../../common/constants'; -import { SecurityLicense } from '../../common/licensing'; - export { Actions } from './actions'; +export { AuthorizationService, AuthorizationServiceSetup } from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { featurePrivilegeIterator } from './privileges'; - -interface SetupAuthorizationParams { - packageVersion: string; - http: CoreSetup['http']; - clusterClient: IClusterClient; - license: SecurityLicense; - loggers: LoggerFactory; - featuresService: FeaturesService; - kibanaIndexName: string; - getSpacesService(): SpacesService | undefined; -} - -export interface Authorization { - actions: Actions; - checkPrivilegesWithRequest: CheckPrivilegesWithRequest; - checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; - checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; - applicationName: string; - mode: AuthorizationMode; - privileges: PrivilegesService; - disableUnauthorizedCapabilities: ( - request: KibanaRequest, - capabilities: UICapabilities - ) => Promise; - registerPrivilegesWithCluster: () => Promise; -} - -export function setupAuthorization({ - http, - packageVersion, - clusterClient, - license, - loggers, - featuresService, - kibanaIndexName, - getSpacesService, -}: SetupAuthorizationParams): Authorization { - const actions = new Actions(packageVersion); - const mode = authorizationModeFactory(license); - const applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - actions, - clusterClient, - applicationName - ); - const privileges = privilegesFactory(actions, featuresService, license); - const logger = loggers.get('authorization'); - - const authz = { - actions, - applicationName, - checkPrivilegesWithRequest, - checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory( - checkPrivilegesWithRequest, - getSpacesService - ), - checkSavedObjectsPrivilegesWithRequest: checkSavedObjectsPrivilegesWithRequestFactory( - checkPrivilegesWithRequest, - getSpacesService - ), - mode, - privileges, - - async disableUnauthorizedCapabilities(request: KibanaRequest, capabilities: UICapabilities) { - // If we have a license which doesn't enable security, or we're a legacy user we shouldn't - // disable any ui capabilities - if (!mode.useRbacForRequest(request)) { - return capabilities; - } - - const disableUICapabilities = disableUICapabilitiesFactory( - request, - featuresService.getFeatures(), - logger, - authz - ); - - if (!request.auth.isAuthenticated) { - return disableUICapabilities.all(capabilities); - } - - return await disableUICapabilities.usingPrivileges(capabilities); - }, - - registerPrivilegesWithCluster: async () => { - const features = featuresService.getFeatures(); - validateFeaturePrivileges(features); - validateReservedPrivileges(features); - - await registerPrivilegesWithCluster(logger, privileges, applicationName, clusterClient); - }, - }; - - initAPIAuthorization(http, authz, loggers.get('api-authorization')); - initAppAuthorization(http, authz, loggers.get('app-authorization'), featuresService); - - return authz; -} diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index b023c12d35b79..06f064a379fe6 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -8,6 +8,8 @@ import { Feature } from '../../../../features/server'; import { Actions } from '../actions'; import { privilegesFactory } from './privileges'; +import { featuresPluginMock } from '../../../../features/server/mocks'; + const actions = new Actions('1.0.0-zeta1'); describe('features', () => { @@ -42,7 +44,9 @@ describe('features', () => { }), ]; - const mockFeaturesService = { getFeatures: jest.fn().mockReturnValue(features) }; + const mockFeaturesService = featuresPluginMock.createSetup(); + mockFeaturesService.getFeatures.mockReturnValue(features); + const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index f3b2881e79ece..5a15290a7f1a2 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -6,11 +6,10 @@ import { uniq } from 'lodash'; import { SecurityLicense } from '../../../common/licensing'; -import { Feature } from '../../../../features/server'; +import { Feature, PluginSetupContract as FeaturesPluginSetup } from '../../../../features/server'; import { RawKibanaPrivileges } from '../../../common/model'; import { Actions } from '../actions'; import { featurePrivilegeBuilderFactory } from './feature_privilege_builder'; -import { FeaturesService } from '../../plugin'; import { featurePrivilegeIterator, subFeaturePrivilegeIterator, @@ -22,7 +21,7 @@ export interface PrivilegesService { export function privilegesFactory( actions: Actions, - featuresService: FeaturesService, + featuresService: FeaturesPluginSetup, licenseService: Pick ) { const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions); diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts index fff4345c72409..e21203e60b887 100644 --- a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts @@ -49,7 +49,7 @@ const registerPrivilegesWithClusterTest = ( }); for (const deletedPrivilege of deletedPrivileges) { expect(mockLogger.debug).toHaveBeenCalledWith( - `Deleting Kibana Privilege ${deletedPrivilege} from Elasticearch for ${application}` + `Deleting Kibana Privilege ${deletedPrivilege} from Elasticsearch for ${application}` ); expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( 'shield.deletePrivilege', @@ -82,7 +82,7 @@ const registerPrivilegesWithClusterTest = ( `Registering Kibana Privileges with Elasticsearch for ${application}` ); expect(mockLogger.debug).toHaveBeenCalledWith( - `Kibana Privileges already registered with Elasticearch for ${application}` + `Kibana Privileges already registered with Elasticsearch for ${application}` ); }; }; diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts index 22e7830d20e28..8e54794494a90 100644 --- a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts @@ -61,14 +61,14 @@ export async function registerPrivilegesWithCluster( privilege: application, }); if (arePrivilegesEqual(existingPrivileges, expectedPrivileges)) { - logger.debug(`Kibana Privileges already registered with Elasticearch for ${application}`); + logger.debug(`Kibana Privileges already registered with Elasticsearch for ${application}`); return; } const privilegesToDelete = getPrivilegesToDelete(existingPrivileges, expectedPrivileges); for (const privilegeToDelete of privilegesToDelete) { logger.debug( - `Deleting Kibana Privilege ${privilegeToDelete} from Elasticearch for ${application}` + `Deleting Kibana Privilege ${privilegeToDelete} from Elasticsearch for ${application}` ); try { await clusterClient.callAsInternalUser('shield.deletePrivilege', { diff --git a/x-pack/plugins/security/server/authorization/service.test.mocks.ts b/x-pack/plugins/security/server/authorization/service.test.mocks.ts index 5cd2eac20094d..d73adde66a490 100644 --- a/x-pack/plugins/security/server/authorization/service.test.mocks.ts +++ b/x-pack/plugins/security/server/authorization/service.test.mocks.ts @@ -28,3 +28,8 @@ export const mockAuthorizationModeFactory = jest.fn(); jest.mock('./mode', () => ({ authorizationModeFactory: mockAuthorizationModeFactory, })); + +export const mockRegisterPrivilegesWithCluster = jest.fn(); +jest.mock('./register_privileges_with_cluster', () => ({ + registerPrivilegesWithCluster: mockRegisterPrivilegesWithCluster, +})); diff --git a/x-pack/plugins/security/server/feature_usage/feature_usage_service.test.ts b/x-pack/plugins/security/server/feature_usage/feature_usage_service.test.ts new file mode 100644 index 0000000000000..46796fa73ef26 --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/feature_usage_service.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecurityFeatureUsageService } from './feature_usage_service'; + +describe('#setup', () => { + it('registers all known security features', () => { + const featureUsage = { register: jest.fn() }; + const securityFeatureUsage = new SecurityFeatureUsageService(); + securityFeatureUsage.setup({ featureUsage }); + expect(featureUsage.register).toHaveBeenCalledTimes(2); + expect(featureUsage.register.mock.calls.map((c) => c[0])).toMatchInlineSnapshot(` + Array [ + "Subfeature privileges", + "Pre-access agreement", + ] + `); + }); +}); + +describe('start contract', () => { + it('notifies when sub-feature privileges are in use', () => { + const featureUsage = { notifyUsage: jest.fn(), getLastUsages: jest.fn() }; + const securityFeatureUsage = new SecurityFeatureUsageService(); + const startContract = securityFeatureUsage.start({ featureUsage }); + startContract.recordSubFeaturePrivilegeUsage(); + expect(featureUsage.notifyUsage).toHaveBeenCalledTimes(1); + expect(featureUsage.notifyUsage).toHaveBeenCalledWith('Subfeature privileges'); + }); + + it('notifies when pre-access agreement is used', () => { + const featureUsage = { notifyUsage: jest.fn(), getLastUsages: jest.fn() }; + const securityFeatureUsage = new SecurityFeatureUsageService(); + const startContract = securityFeatureUsage.start({ featureUsage }); + startContract.recordPreAccessAgreementUsage(); + expect(featureUsage.notifyUsage).toHaveBeenCalledTimes(1); + expect(featureUsage.notifyUsage).toHaveBeenCalledWith('Pre-access agreement'); + }); +}); diff --git a/x-pack/plugins/security/server/feature_usage/feature_usage_service.ts b/x-pack/plugins/security/server/feature_usage/feature_usage_service.ts new file mode 100644 index 0000000000000..1bc1e664981bf --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/feature_usage_service.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from '../../../licensing/server'; + +interface SetupDeps { + featureUsage: FeatureUsageServiceSetup; +} + +interface StartDeps { + featureUsage: FeatureUsageServiceStart; +} + +export interface SecurityFeatureUsageServiceStart { + recordPreAccessAgreementUsage: () => void; + recordSubFeaturePrivilegeUsage: () => void; +} + +export class SecurityFeatureUsageService { + public setup({ featureUsage }: SetupDeps) { + featureUsage.register('Subfeature privileges', 'gold'); + featureUsage.register('Pre-access agreement', 'gold'); + } + + public start({ featureUsage }: StartDeps): SecurityFeatureUsageServiceStart { + return { + recordPreAccessAgreementUsage() { + featureUsage.notifyUsage('Pre-access agreement'); + }, + recordSubFeaturePrivilegeUsage() { + featureUsage.notifyUsage('Subfeature privileges'); + }, + }; + } +} diff --git a/x-pack/plugins/security/server/feature_usage/index.mock.ts b/x-pack/plugins/security/server/feature_usage/index.mock.ts new file mode 100644 index 0000000000000..6ed42145abd76 --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/index.mock.ts @@ -0,0 +1,16 @@ +/* + * 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 { SecurityFeatureUsageServiceStart } from './feature_usage_service'; + +export const securityFeatureUsageServiceMock = { + createStartContract() { + return { + recordPreAccessAgreementUsage: jest.fn(), + recordSubFeaturePrivilegeUsage: jest.fn(), + } as jest.Mocked; + }, +}; diff --git a/x-pack/plugins/security/server/feature_usage/index.ts b/x-pack/plugins/security/server/feature_usage/index.ts new file mode 100644 index 0000000000000..a3e1f35ee3824 --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + SecurityFeatureUsageService, + SecurityFeatureUsageServiceStart, +} from './feature_usage_service'; diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index 72a946d6c5155..c2d99433b0346 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SecurityPluginSetup } from './plugin'; - import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; import { licenseMock } from '../common/licensing/index.mock'; @@ -23,7 +21,6 @@ function createSetupMock() { }, registerSpacesService: jest.fn(), license: licenseMock.create(), - __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 3e30ff9447f3e..e01c608e5f306 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -41,16 +41,15 @@ describe('Security Plugin', () => { mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); - mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; + mockDependencies = ({ + licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, + } as unknown) as PluginSetupDependencies; }); describe('setup()', () => { it('exposes proper contract', async () => { await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { - "__legacyCompat": Object { - "registerPrivilegesWithCluster": [Function], - }, "audit": Object { "getLogger": [Function], }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index bdda0be9b15a7..c8f47aaae7b5d 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -8,32 +8,35 @@ import { combineLatest } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; import { + deepFreeze, ICustomClusterClient, CoreSetup, + CoreStart, Logger, PluginInitializerContext, } from '../../../../src/core/server'; -import { deepFreeze } from '../../../../src/core/server'; import { SpacesPluginSetup } from '../../spaces/server'; -import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../features/server'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { Authentication, setupAuthentication } from './authentication'; -import { Authorization, setupAuthorization } from './authorization'; +import { AuthorizationService, AuthorizationServiceSetup } from './authorization'; import { ConfigSchema, createConfig } from './config'; import { defineRoutes } from './routes'; import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; import { setupSavedObjects } from './saved_objects'; import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; +import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage'; export type SpacesService = Pick< SpacesPluginSetup['spacesService'], 'getSpaceId' | 'namespaceToSpaceId' >; -export type FeaturesService = Pick; - /** * Describes public Security plugin contract returned at the `setup` stage. */ @@ -48,7 +51,7 @@ export interface SecurityPluginSetup { | 'grantAPIKeyAsInternalUser' | 'invalidateAPIKeyAsInternalUser' >; - authz: Pick; + authz: Pick; license: SecurityLicense; audit: Pick; @@ -61,17 +64,18 @@ export interface SecurityPluginSetup { * @param service Spaces service exposed by the Spaces plugin. */ registerSpacesService: (service: SpacesService) => void; - - __legacyCompat: { - registerPrivilegesWithCluster: () => void; - }; } export interface PluginSetupDependencies { - features: FeaturesService; + features: FeaturesPluginSetup; licensing: LicensingPluginSetup; } +export interface PluginStartDependencies { + features: FeaturesPluginStart; + licensing: LicensingPluginStart; +} + /** * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ @@ -80,7 +84,18 @@ export class Plugin { private clusterClient?: ICustomClusterClient; private spacesService?: SpacesService | symbol = Symbol('not accessed'); private securityLicenseService?: SecurityLicenseService; + + private readonly featureUsageService = new SecurityFeatureUsageService(); + private featureUsageServiceStart?: SecurityFeatureUsageServiceStart; + private readonly getFeatureUsageService = () => { + if (!this.featureUsageServiceStart) { + throw new Error(`featureUsageServiceStart is not registered!`); + } + return this.featureUsageServiceStart; + }; + private readonly auditService = new AuditService(this.initializerContext.logger.get('audit')); + private readonly authorizationService = new AuthorizationService(); private readonly getSpacesService = () => { // Changing property value from Symbol to undefined denotes the fact that property was accessed. @@ -95,7 +110,10 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup, { features, licensing }: PluginSetupDependencies) { + public async setup( + core: CoreSetup, + { features, licensing }: PluginSetupDependencies + ) { const [config, legacyConfig] = await combineLatest([ this.initializerContext.config.create>().pipe( map((rawConfig) => @@ -118,11 +136,14 @@ export class Plugin { license$: licensing.license$, }); + this.featureUsageService.setup({ featureUsage: licensing.featureUsage }); + const audit = this.auditService.setup({ license, config: config.audit }); const auditLogger = new SecurityAuditLogger(audit.getLogger()); const authc = await setupAuthentication({ auditLogger, + getFeatureUsageService: this.getFeatureUsageService, http: core.http, clusterClient: this.clusterClient, config, @@ -130,15 +151,17 @@ export class Plugin { loggers: this.initializerContext.logger, }); - const authz = await setupAuthorization({ + const authz = this.authorizationService.setup({ http: core.http, + capabilities: core.capabilities, + status: core.status, clusterClient: this.clusterClient, license, loggers: this.initializerContext.logger, kibanaIndexName: legacyConfig.kibana.index, packageVersion: this.initializerContext.env.packageInfo.version, getSpacesService: this.getSpacesService, - featuresService: features, + features, }); setupSavedObjects({ @@ -148,8 +171,6 @@ export class Plugin { getSpacesService: this.getSpacesService, }); - core.capabilities.registerSwitcher(authz.disableUnauthorizedCapabilities); - defineRoutes({ router: core.http.createRouter(), basePath: core.http.basePath, @@ -160,6 +181,11 @@ export class Plugin { authc, authz, license, + getFeatures: () => + core + .getStartServices() + .then(([, { features: featuresStart }]) => featuresStart.getFeatures()), + getFeatureUsageService: this.getFeatureUsageService, }); return deepFreeze({ @@ -192,15 +218,15 @@ export class Plugin { this.spacesService = service; }, - - __legacyCompat: { - registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(), - }, }); } - public start() { + public start(core: CoreStart, { features, licensing }: PluginStartDependencies) { this.logger.debug('Starting plugin'); + this.featureUsageServiceStart = this.featureUsageService.start({ + featureUsage: licensing.featureUsage, + }); + this.authorizationService.start({ features, clusterClient: this.clusterClient! }); } public stop() { @@ -216,7 +242,11 @@ export class Plugin { this.securityLicenseService = undefined; } + if (this.featureUsageServiceStart) { + this.featureUsageServiceStart = undefined; + } this.auditService.stop(); + this.authorizationService.stop(); } private wasSpacesServiceAccessed() { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index d7710bf669ce1..bec60fa149bcf 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -15,6 +15,8 @@ import { httpServerMock, } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; +import { Feature } from '../../../../../features/server'; +import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock'; const application = 'kibana-.kibana'; const privilegeMap = { @@ -47,7 +49,12 @@ interface TestOptions { licenseCheckResult?: LicenseCheck; apiResponses?: Array<() => Promise>; payload?: Record; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + asserts: { + statusCode: number; + result?: Record; + apiArguments?: unknown[][]; + recordSubFeaturePrivilegeUsage?: boolean; + }; } const putRoleTest = ( @@ -71,6 +78,47 @@ const putRoleTest = ( mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); } + mockRouteDefinitionParams.getFeatureUsageService.mockReturnValue( + securityFeatureUsageServiceMock.createStartContract() + ); + + mockRouteDefinitionParams.getFeatures.mockResolvedValue([ + new Feature({ + id: 'feature_1', + name: 'feature 1', + app: [], + privileges: { + all: { + ui: [], + savedObject: { all: [], read: [] }, + }, + read: { + ui: [], + savedObject: { all: [], read: [] }, + }, + }, + subFeatures: [ + { + name: 'sub feature 1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'sub_feature_privilege_1', + name: 'first sub-feature privilege', + includeIn: 'none', + ui: [], + savedObject: { all: [], read: [] }, + }, + ], + }, + ], + }, + ], + }), + ]); + definePutRolesRoutes(mockRouteDefinitionParams); const [[{ validate }, handler]] = mockRouteDefinitionParams.router.put.mock.calls; @@ -99,6 +147,16 @@ const putRoleTest = ( expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + + if (asserts.recordSubFeaturePrivilegeUsage) { + expect( + mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage + ).toHaveBeenCalledTimes(1); + } else { + expect( + mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage + ).not.toHaveBeenCalled(); + } }); }; @@ -598,5 +656,131 @@ describe('PUT role', () => { result: undefined, }, }); + + putRoleTest(`notifies when sub-feature privileges are included`, { + name: 'foo-role', + payload: { + kibana: [ + { + spaces: ['*'], + feature: { + feature_1: ['sub_feature_privilege_1'], + }, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + recordSubFeaturePrivilegeUsage: true, + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_feature_1.sub_feature_privilege_1'], + resources: ['*'], + }, + ], + metadata: undefined, + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`does not record sub-feature privilege usage for unknown privileges`, { + name: 'foo-role', + payload: { + kibana: [ + { + spaces: ['*'], + feature: { + feature_1: ['unknown_sub_feature_privilege_1'], + }, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + recordSubFeaturePrivilegeUsage: false, + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_feature_1.unknown_sub_feature_privilege_1'], + resources: ['*'], + }, + ], + metadata: undefined, + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`does not record sub-feature privilege usage for unknown features`, { + name: 'foo-role', + payload: { + kibana: [ + { + spaces: ['*'], + feature: { + unknown_feature: ['sub_feature_privilege_1'], + }, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + recordSubFeaturePrivilegeUsage: false, + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_unknown_feature.sub_feature_privilege_1'], + resources: ['*'], + }, + ], + metadata: undefined, + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); }); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index 5db83375afa96..d83cf92bcaa0d 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { Feature } from '../../../../../features/common'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; @@ -14,7 +15,37 @@ import { transformPutPayloadToElasticsearchRole, } from './model'; -export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +const roleGrantsSubFeaturePrivileges = ( + features: Feature[], + role: TypeOf> +) => { + if (!role.kibana) { + return false; + } + + const subFeaturePrivileges = new Map( + features.map((feature) => [ + feature.id, + feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2), + ]) + ); + + const hasAnySubFeaturePrivileges = role.kibana.some((kibanaPrivilege) => + Object.entries(kibanaPrivilege.feature ?? {}).some(([featureId, privileges]) => { + return !!subFeaturePrivileges.get(featureId)?.some(({ id }) => privileges.includes(id)); + }) + ); + + return hasAnySubFeaturePrivileges; +}; + +export function definePutRolesRoutes({ + router, + authz, + clusterClient, + getFeatures, + getFeatureUsageService, +}: RouteDefinitionParams) { router.put( { path: '/api/security/role/{name}', @@ -46,9 +77,16 @@ export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefi rawRoles[name] ? rawRoles[name].applications : [] ); - await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.putRole', { name: request.params.name, body }); + const [features] = await Promise.all([ + getFeatures(), + clusterClient + .asScoped(request) + .callAsCurrentUser('shield.putRole', { name: request.params.name, body }), + ]); + + if (roleGrantsSubFeaturePrivileges(features, request.body)) { + getFeatureUsageService().recordSubFeaturePrivilegeUsage(); + } return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index b0c74b98ee19b..1a93d6701e257 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -29,5 +29,7 @@ export const routeDefinitionParamsMock = { authz: authorizationMock.create(), license: licenseMock.create(), httpResources: httpResourcesMock.createRegistrar(), + getFeatures: jest.fn(), + getFeatureUsageService: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index e43072b95c906..5721a2699d15c 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Feature } from '../../../features/server'; import { CoreSetup, HttpResources, @@ -13,7 +14,7 @@ import { } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { Authentication } from '../authentication'; -import { Authorization } from '../authorization'; +import { AuthorizationServiceSetup } from '../authorization'; import { ConfigType } from '../config'; import { defineAuthenticationRoutes } from './authentication'; @@ -23,6 +24,7 @@ import { defineIndicesRoutes } from './indices'; import { defineUsersRoutes } from './users'; import { defineRoleMappingRoutes } from './role_mapping'; import { defineViewRoutes } from './views'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; /** * Describes parameters used to define HTTP routes. @@ -35,8 +37,10 @@ export interface RouteDefinitionParams { clusterClient: IClusterClient; config: ConfigType; authc: Authentication; - authz: Authorization; + authz: AuthorizationServiceSetup; license: SecurityLicense; + getFeatures: () => Promise; + getFeatureUsageService: () => SecurityFeatureUsageServiceStart; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index 29fbe3af21b95..6acfd06a0309b 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -11,13 +11,16 @@ import { SavedObjectsClient, } from '../../../../../src/core/server'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; -import { Authorization } from '../authorization'; +import { AuthorizationServiceSetup } from '../authorization'; import { SecurityAuditLogger } from '../audit'; import { SpacesService } from '../plugin'; interface SetupSavedObjectsParams { auditLogger: SecurityAuditLogger; - authz: Pick; + authz: Pick< + AuthorizationServiceSetup, + 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest' + >; savedObjects: CoreSetup['savedObjects']; getSpacesService(): SpacesService | undefined; } diff --git a/x-pack/plugins/security_solution/.gitattributes b/x-pack/plugins/security_solution/.gitattributes new file mode 100644 index 0000000000000..431f25be5e78e --- /dev/null +++ b/x-pack/plugins/security_solution/.gitattributes @@ -0,0 +1,6 @@ +# Auto-collapse generated files in GitHub +# https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github +x-pack/plugins/security_solution/server/graphql/types.ts linguist-generated=true +x-pack/plugins/security_solution/public/graphql/types.ts linguist-generated=true +x-pack/plugins/security_solution/public/graphql/introspection.json linguist-generated=true + diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts new file mode 100644 index 0000000000000..d04d1f2c91b97 --- /dev/null +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -0,0 +1,145 @@ +/* + * 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 APP_ID = 'securitySolution'; +export const APP_NAME = 'Security'; +export const APP_ICON = 'securityAnalyticsApp'; +export const APP_PATH = `/app/security`; +export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; +export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; +export const DEFAULT_DATE_FORMAT = 'dateFormat'; +export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; +export const DEFAULT_DARK_MODE = 'theme:darkMode'; +export const DEFAULT_INDEX_KEY = 'securitySolution:defaultIndex'; +export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern'; +export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; +export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; +export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults'; +export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults'; +export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; +export const DEFAULT_MAX_SIGNALS = 100; +export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; +export const DEFAULT_ANOMALY_SCORE = 'securitySolution:defaultAnomalyScore'; +export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; +export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; +export const DEFAULT_FROM = 'now-24h'; +export const DEFAULT_TO = 'now'; +export const DEFAULT_INTERVAL_PAUSE = true; +export const DEFAULT_INTERVAL_TYPE = 'manual'; +export const DEFAULT_INTERVAL_VALUE = 300000; // ms +export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; +export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; + +/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ +export const DEFAULT_INDEX_PATTERN = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + +/** This Kibana Advanced Setting enables the `Security news` feed widget */ +export const ENABLE_NEWS_FEED_SETTING = 'securitySolution:enableNewsFeed'; + +/** This Kibana Advanced Setting specifies the URL of the News feed widget */ +export const NEWS_FEED_URL_SETTING = 'securitySolution:newsFeedUrl'; + +/** The default value for News feed widget */ +export const NEWS_FEED_URL_SETTING_DEFAULT = 'https://feeds.elastic.co/security-solution'; + +/** This Kibana Advanced Setting specifies the URLs of `IP Reputation Links`*/ +export const IP_REPUTATION_LINKS_SETTING = 'securitySolution:ipReputationLinks'; + +/** The default value for `IP Reputation Links` */ +export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[ + { "name": "virustotal.com", "url_template": "https://www.virustotal.com/gui/search/{{ip}}" }, + { "name": "talosIntelligence.com", "url_template": "https://talosintelligence.com/reputation_center/lookup?search={{ip}}" } +]`; + +/** + * Id for the signals alerting type + */ +export const SIGNALS_ID = `siem.signals`; + +/** + * Id for the notifications alerting type + */ +export const NOTIFICATIONS_ID = `siem.notifications`; + +/** + * Special internal structure for tags for signals. This is used + * to filter out tags that have internal structures within them. + */ +export const INTERNAL_IDENTIFIER = '__internal'; +export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`; +export const INTERNAL_RULE_ALERT_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_alert_id`; +export const INTERNAL_IMMUTABLE_KEY = `${INTERNAL_IDENTIFIER}_immutable`; + +/** + * Detection engine routes + */ +export const DETECTION_ENGINE_URL = '/api/detection_engine'; +export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; +export const DETECTION_ENGINE_PREPACKAGED_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged`; +export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`; +export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; +export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`; +export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/_find_statuses`; +export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`; + +export const TIMELINE_URL = '/api/timeline'; +export const TIMELINE_DRAFT_URL = `${TIMELINE_URL}/_draft`; +export const TIMELINE_EXPORT_URL = `${TIMELINE_URL}/_export`; +export const TIMELINE_IMPORT_URL = `${TIMELINE_URL}/_import`; + +/** + * Default signals index key for kibana.dev.yml + */ +export const SIGNALS_INDEX_KEY = 'signalsIndex'; +export const DETECTION_ENGINE_SIGNALS_URL = `${DETECTION_ENGINE_URL}/signals`; +export const DETECTION_ENGINE_SIGNALS_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/status`; +export const DETECTION_ENGINE_QUERY_SIGNALS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/search`; + +/** + * Common naming convention for an unauthenticated user + */ +export const UNAUTHENTICATED_USER = 'Unauthenticated'; + +/* + Licensing requirements + */ +export const MINIMUM_ML_LICENSE = 'platinum'; + +/* + Rule notifications options +*/ +export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ + '.email', + '.slack', + '.pagerduty', + '.webhook', +]; +export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; +export const NOTIFICATION_THROTTLE_RULE = 'rule'; + +/** + * Histograms for fields named in this list should be displayed with an + * "All others" bucket, to count events that don't specify a value for + * the field being counted + */ +export const showAllOthersBucket: string[] = [ + 'destination.ip', + 'event.action', + 'event.category', + 'event.dataset', + 'event.module', + 'signal.rule.threat.tactic.name', + 'source.ip', + 'destination.ip', + 'user.name', +]; diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/__mocks__/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/__mocks__/utils.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/__mocks__/utils.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/__mocks__/utils.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/postive_integer.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/postive_integer.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts diff --git a/x-pack/plugins/siem/common/detection_engine/transform_actions.test.ts b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.test.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/transform_actions.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/transform_actions.test.ts diff --git a/x-pack/plugins/siem/common/detection_engine/transform_actions.ts b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/transform_actions.ts rename to x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts diff --git a/x-pack/plugins/siem/common/detection_engine/types.ts b/x-pack/plugins/security_solution/common/detection_engine/types.ts similarity index 100% rename from x-pack/plugins/siem/common/detection_engine/types.ts rename to x-pack/plugins/security_solution/common/detection_engine/types.ts diff --git a/x-pack/plugins/siem/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts similarity index 89% rename from x-pack/plugins/siem/common/endpoint/generate_data.test.ts rename to x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 3fcb00d879583..6c8c5e3f51808 100644 --- a/x-pack/plugins/siem/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -3,7 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EndpointDocGenerator, Event, Tree, TreeNode } from './generate_data'; +import { + EndpointDocGenerator, + Event, + Tree, + TreeNode, + RelatedEventCategory, + ECSCategory, +} from './generate_data'; interface Node { events: Event[]; @@ -106,7 +113,11 @@ describe('data generator', () => { generations, percentTerminated: 100, percentWithRelated: 100, - relatedEvents: 4, + relatedEvents: [ + { category: RelatedEventCategory.Driver, count: 1 }, + { category: RelatedEventCategory.File, count: 2 }, + { category: RelatedEventCategory.Network, count: 1 }, + ], }); }); @@ -117,6 +128,36 @@ describe('data generator', () => { return (inRelated || inLifecycle) && event.process.entity_id === node.id; }; + it('has the right related events for each node', () => { + const checkRelatedEvents = (node: TreeNode) => { + expect(node.relatedEvents.length).toEqual(4); + + const counts: Record = {}; + for (const event of node.relatedEvents) { + if (Array.isArray(event.event.category)) { + for (const cat of event.event.category) { + counts[cat] = counts[cat] + 1 || 1; + } + } else { + counts[event.event.category] = counts[event.event.category] + 1 || 1; + } + } + expect(counts[ECSCategory.Driver]).toEqual(1); + expect(counts[ECSCategory.File]).toEqual(2); + expect(counts[ECSCategory.Network]).toEqual(1); + }; + + for (const node of tree.ancestry.values()) { + checkRelatedEvents(node); + } + + for (const node of tree.children.values()) { + checkRelatedEvents(node); + } + + checkRelatedEvents(tree.origin); + }); + it('has the right number of ancestors', () => { expect(tree.ancestry.size).toEqual(ancestors); }); diff --git a/x-pack/plugins/siem/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts similarity index 82% rename from x-pack/plugins/siem/common/endpoint/generate_data.ts rename to x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 597ad4df64dfe..b17a5aa28ac6a 100644 --- a/x-pack/plugins/siem/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -24,7 +24,7 @@ interface EventOptions { entityID?: string; parentEntityID?: string; eventType?: string; - eventCategory?: string; + eventCategory?: string | string[]; processName?: string; } @@ -75,21 +75,98 @@ const POLICIES: Array<{ name: string; id: string }> = [ const FILE_OPERATIONS: string[] = ['creation', 'open', 'rename', 'execution', 'deletion']; interface EventInfo { - category: string; + category: string | string[]; /** * This denotes the `event.type` field for when an event is created, this can be `start` or `creation` */ creationType: string; } +/** + * The valid ecs categories. + */ +export enum ECSCategory { + Driver = 'driver', + File = 'file', + Network = 'network', + /** + * Registry has not been added to ecs yet. + */ + Registry = 'registry', + Authentication = 'authentication', + Session = 'session', +} + +/** + * High level categories for related events. These specify the type of related events that should be generated. + */ +export enum RelatedEventCategory { + /** + * The Random category allows the related event categories to be chosen randomly + */ + Random = 'random', + Driver = 'driver', + File = 'file', + Network = 'network', + Registry = 'registry', + /** + * Security isn't an actual category but defines a type of related event to be created. + */ + Security = 'security', +} + +/** + * This map defines the relationship between a higher level event type defined by the RelatedEventCategory enums and + * the ECS categories that is should map to. This should only be used for tests that need to determine the exact + * ecs categories that were created based on the related event information passed to the generator. + */ +export const categoryMapping: Record = { + [RelatedEventCategory.Security]: [ECSCategory.Authentication, ECSCategory.Session], + [RelatedEventCategory.Driver]: ECSCategory.Driver, + [RelatedEventCategory.File]: ECSCategory.File, + [RelatedEventCategory.Network]: ECSCategory.Network, + [RelatedEventCategory.Registry]: ECSCategory.Registry, + /** + * Random is only used by the generator to indicate that it should randomly choose the event information when generating + * related events. It does not map to a specific ecs category. + */ + [RelatedEventCategory.Random]: '', +}; + +/** + * The related event category and number of events that should be generated. + */ +export interface RelatedEventInfo { + category: RelatedEventCategory; + count: number; +} + // These are from the v1 schemas and aren't all valid ECS event categories, still in flux -const OTHER_EVENT_CATEGORIES: EventInfo[] = [ - { category: 'driver', creationType: 'start' }, - { category: 'file', creationType: 'creation' }, - { category: 'library', creationType: 'start' }, - { category: 'network', creationType: 'start' }, - { category: 'registry', creationType: 'creation' }, -]; +const OTHER_EVENT_CATEGORIES: Record< + Exclude, + EventInfo +> = { + [RelatedEventCategory.Security]: { + category: categoryMapping[RelatedEventCategory.Security], + creationType: 'start', + }, + [RelatedEventCategory.Driver]: { + category: categoryMapping[RelatedEventCategory.Driver], + creationType: 'start', + }, + [RelatedEventCategory.File]: { + category: categoryMapping[RelatedEventCategory.File], + creationType: 'creation', + }, + [RelatedEventCategory.Network]: { + category: categoryMapping[RelatedEventCategory.Network], + creationType: 'start', + }, + [RelatedEventCategory.Registry]: { + category: categoryMapping[RelatedEventCategory.Registry], + creationType: 'creation', + }, +}; interface HostInfo { elastic: { @@ -164,7 +241,7 @@ export interface TreeOptions { ancestors?: number; generations?: number; children?: number; - relatedEvents?: number; + relatedEvents?: RelatedEventInfo[]; percentWithRelated?: number; percentTerminated?: number; alwaysGenMaxChildrenPerNode?: boolean; @@ -445,6 +522,41 @@ export class EndpointDocGenerator { }; } + /** + * Wrapper generator for fullResolverTreeGenerator to make it easier to quickly stream + * many resolver trees to Elasticsearch. + * @param numAlerts - number of alerts to generate + * @param alertAncestors - number of ancestor generations to create relative to the alert + * @param childGenerations - number of child generations to create relative to the alert + * @param maxChildrenPerNode - maximum number of children for any given node in the tree + * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param percentNodesWithRelated - percent of nodes which should have related events + * @param percentTerminated - percent of nodes which will have process termination events + * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children + */ + public *alertsGenerator( + numAlerts: number, + alertAncestors?: number, + childGenerations?: number, + maxChildrenPerNode?: number, + relatedEventsPerNode?: number, + percentNodesWithRelated?: number, + percentTerminated?: number, + alwaysGenMaxChildrenPerNode?: boolean + ) { + for (let i = 0; i < numAlerts; i++) { + yield* this.fullResolverTreeGenerator( + alertAncestors, + childGenerations, + maxChildrenPerNode, + relatedEventsPerNode, + percentNodesWithRelated, + percentTerminated, + alwaysGenMaxChildrenPerNode + ); + } + } + /** * Generator function that creates the full set of events needed to render resolver. * The number of nodes grows exponentially with the number of generations and children per node. @@ -452,7 +564,8 @@ export class EndpointDocGenerator { * @param alertAncestors - number of ancestor generations to create relative to the alert * @param childGenerations - number of child generations to create relative to the alert * @param maxChildrenPerNode - maximum number of children for any given node in the tree - * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param relatedEventsPerNode - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param percentNodesWithRelated - percent of nodes which should have related events * @param percentTerminated - percent of nodes which will have process termination events * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children @@ -461,7 +574,7 @@ export class EndpointDocGenerator { alertAncestors?: number, childGenerations?: number, maxChildrenPerNode?: number, - relatedEventsPerNode?: number, + relatedEventsPerNode?: RelatedEventInfo[] | number, percentNodesWithRelated?: number, percentTerminated?: number, alwaysGenMaxChildrenPerNode?: boolean @@ -490,13 +603,14 @@ export class EndpointDocGenerator { /** * Creates an alert event and associated process ancestry. The alert event will always be the last event in the return array. * @param alertAncestors - number of ancestor generations to create - * @param relatedEventsPerNode - number of related events to add to each process node being created + * @param relatedEventsPerNode - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param pctWithRelated - percent of ancestors that will have related events * @param pctWithTerminated - percent of ancestors that will have termination events */ public createAlertEventAncestry( alertAncestors = 3, - relatedEventsPerNode = 5, + relatedEventsPerNode: RelatedEventInfo[] | number = 5, pctWithRelated = 30, pctWithTerminated = 100 ): Event[] { @@ -576,7 +690,8 @@ export class EndpointDocGenerator { * @param root - The process event to use as the root node of the tree * @param generations - number of child generations to create. The root node is not counted as a generation. * @param maxChildrenPerNode - maximum number of children for any given node in the tree - * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param relatedEventsPerNode - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param percentNodesWithRelated - percent of nodes which should have related events * @param percentChildrenTerminated - percent of nodes which will have process termination events * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children @@ -585,7 +700,7 @@ export class EndpointDocGenerator { root: Event, generations = 2, maxChildrenPerNode = 2, - relatedEventsPerNode = 3, + relatedEventsPerNode: RelatedEventInfo[] | number = 3, percentNodesWithRelated = 100, percentChildrenTerminated = 100, alwaysGenMaxChildrenPerNode = false @@ -651,25 +766,40 @@ export class EndpointDocGenerator { /** * Creates related events for a process event * @param node - process event to relate events to by entityID - * @param numRelatedEvents - number of related events to generate + * @param relatedEvents - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param processDuration - maximum number of seconds after process event that related event timestamp can be */ public *relatedEventsGenerator( node: Event, - numRelatedEvents = 10, + relatedEvents: RelatedEventInfo[] | number = 10, processDuration: number = 6 * 3600 ) { - for (let i = 0; i < numRelatedEvents; i++) { - const eventInfo = this.randomChoice(OTHER_EVENT_CATEGORIES); - - const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; - yield this.generateEvent({ - timestamp: ts, - entityID: node.process.entity_id, - parentEntityID: node.process.parent?.entity_id, - eventCategory: eventInfo.category, - eventType: eventInfo.creationType, - }); + let relatedEventsInfo: RelatedEventInfo[]; + if (typeof relatedEvents === 'number') { + relatedEventsInfo = [{ category: RelatedEventCategory.Random, count: relatedEvents }]; + } else { + relatedEventsInfo = relatedEvents; + } + for (const event of relatedEventsInfo) { + let eventInfo: EventInfo; + + for (let i = 0; i < event.count; i++) { + if (event.category === RelatedEventCategory.Random) { + eventInfo = this.randomChoice(Object.values(OTHER_EVENT_CATEGORIES)); + } else { + eventInfo = OTHER_EVENT_CATEGORIES[event.category]; + } + + const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + yield this.generateEvent({ + timestamp: ts, + entityID: node.process.entity_id, + parentEntityID: node.process.parent?.entity_id, + eventCategory: eventInfo.category, + eventType: eventInfo.creationType, + }); + } } } @@ -799,7 +929,7 @@ export class EndpointDocGenerator { status: HostPolicyResponseActionStatus.success, }, { - name: 'load_malware_mode', + name: 'load_malware_model', message: 'Error deserializing EXE model; no valid malware model installed', status: HostPolicyResponseActionStatus.success, }, diff --git a/x-pack/plugins/siem/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts similarity index 100% rename from x-pack/plugins/siem/common/endpoint/models/event.ts rename to x-pack/plugins/security_solution/common/endpoint/models/event.ts diff --git a/x-pack/plugins/siem/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts similarity index 100% rename from x-pack/plugins/siem/common/endpoint/models/policy_config.ts rename to x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts diff --git a/x-pack/plugins/siem/common/endpoint/schema/policy.ts b/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts similarity index 100% rename from x-pack/plugins/siem/common/endpoint/schema/policy.ts rename to x-pack/plugins/security_solution/common/endpoint/schema/policy.ts diff --git a/x-pack/plugins/siem/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts similarity index 100% rename from x-pack/plugins/siem/common/endpoint/schema/resolver.ts rename to x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts new file mode 100644 index 0000000000000..816f9b77115ec --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -0,0 +1,733 @@ +/* + * 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 { Datasource, NewDatasource } from '../../../ingest_manager/common'; + +/** + * Object that allows you to maintain stateful information in the location object across navigation events + * + */ + +export interface AppLocation { + pathname: string; + search: string; + hash: string; + key?: string; + state?: { + isTabChange?: boolean; + }; +} + +/** + * A deep readonly type that will make all children of a given object readonly recursively + */ +export type Immutable = T extends undefined | null | boolean | string | number + ? T + : unknown extends T + ? unknown + : T extends Array + ? ImmutableArray + : T extends Map + ? ImmutableMap + : T extends Set + ? ImmutableSet + : ImmutableObject; + +export type ImmutableArray = ReadonlyArray>; +type ImmutableMap = ReadonlyMap, Immutable>; +type ImmutableSet = ReadonlySet>; +type ImmutableObject = { readonly [K in keyof T]: Immutable }; + +export interface EventStats { + /** + * The total number of related events (all events except process and alerts) that exist for a node. + */ + total: number; + /** + * A mapping of ECS event.category to the number of related events are marked with that category + * For example: + * { + * network: 5, + * file: 2 + * } + */ + byCategory: Record; +} + +/** + * Statistical information for a node in a resolver tree. + */ +export interface ResolverNodeStats { + /** + * The stats for related events (excludes alerts and process events) for a particular node in the resolver tree. + */ + events: EventStats; + /** + * The total number of alerts that exist for a node. + */ + totalAlerts: number; +} + +/** + * A child node can also have additional children so we need to provide a pagination cursor. + */ +export interface ChildNode extends LifecycleNode { + /** + * A child node's pagination cursor can be null for a couple reasons: + * 1. At the time of querying it could have no children in ES, in which case it will be marked as + * null because we know it does not have children during this query. + * 2. If the max level was reached we do not know if this node has children or not so we'll mark it as null + */ + nextChild: string | null; +} + +/** + * The response structure for the children route. The structure is an array of nodes where each node + * has an array of lifecycle events. + */ +export interface ResolverChildren { + childNodes: ChildNode[]; + /** + * This is the children cursor for the origin of a tree. + */ + nextChild: string | null; +} + +/** + * A flattened tree representing the nodes in a resolver graph. + */ +export interface ResolverTree { + /** + * Origin of the tree. This is in the middle of the tree. Typically this would be the same + * process node that generated an alert. + */ + entityID: string; + children: ResolverChildren; + relatedEvents: Omit; + ancestry: ResolverAncestry; + lifecycle: ResolverEvent[]; + stats: ResolverNodeStats; +} + +/** + * The lifecycle events (start, end etc) for a node. + */ +export interface LifecycleNode { + entityID: string; + lifecycle: ResolverEvent[]; + /** + * stats are only set when the entire tree is being fetched + */ + stats?: ResolverNodeStats; +} + +/** + * The response structure when searching for ancestors of a node. + */ +export interface ResolverAncestry { + /** + * An array of ancestors with the lifecycle events grouped together + */ + ancestors: LifecycleNode[]; + /** + * A cursor for retrieving additional ancestors for a particular node. `null` indicates that there were no additional + * ancestors when the request returned. More could have been ingested by ES after the fact though. + */ + nextAncestor: string | null; +} + +/** + * Response structure for the related events route. + */ +export interface ResolverRelatedEvents { + entityID: string; + events: ResolverEvent[]; + nextEvent: string | null; +} + +/** + * Returned by the server via /api/endpoint/metadata + */ +export interface HostResultList { + /* the hosts restricted by the page size */ + hosts: HostInfo[]; + /* the total number of unique hosts in the index */ + total: number; + /* the page size requested */ + request_page_size: number; + /* the page index requested */ + request_page_index: number; +} + +/** + * Operating System metadata for a host. + */ +export interface HostOS { + full: string; + name: string; + version: string; + variant: string; +} + +/** + * Host metadata. Describes an endpoint host. + */ +export interface Host { + id: string; + hostname: string; + ip: string[]; + mac: string[]; + os: HostOS; +} + +/** + * A record of hashes for something. Provides hashes in multiple formats. A favorite structure of the Elastic Endpoint. + */ +interface Hashes { + /** + * A hash in MD5 format. + */ + md5: string; + /** + * A hash in SHA-1 format. + */ + sha1: string; + /** + * A hash in SHA-256 format. + */ + sha256: string; +} + +interface MalwareClassification { + identifier: string; + score: number; + threshold: number; + version: string; +} + +interface ThreadFields { + id: number; + service_name: string; + start: number; + start_address: number; + start_address_module: string; +} + +interface DllFields { + pe: { + architecture: string; + imphash: string; + }; + code_signature: { + subject_name: string; + trusted: boolean; + }; + compile_time: number; + hash: Hashes; + malware_classification: MalwareClassification; + mapped_address: number; + mapped_size: number; + path: string; +} + +/** + * Describes an Alert Event. + */ +export type AlertEvent = Immutable<{ + '@timestamp': number; + agent: { + id: string; + version: string; + }; + event: { + id: string; + action: string; + category: string; + kind: string; + dataset: string; + module: string; + type: string; + }; + endpoint: { + policy: { + id: string; + }; + }; + process: { + code_signature: { + subject_name: string; + trusted: boolean; + }; + command_line?: string; + domain?: string; + pid: number; + ppid?: number; + entity_id: string; + parent?: { + pid: number; + entity_id: string; + }; + name: string; + hash: Hashes; + pe?: { + imphash: string; + }; + executable: string; + sid?: string; + start: number; + malware_classification?: MalwareClassification; + token: { + domain: string; + type: string; + user: string; + sid: string; + integrity_level: number; + integrity_level_name: string; + privileges?: Array<{ + description: string; + name: string; + enabled: boolean; + }>; + }; + thread?: ThreadFields[]; + uptime: number; + user: string; + }; + file: { + owner: string; + name: string; + path: string; + accessed: number; + mtime: number; + created: number; + size: number; + hash: Hashes; + pe?: { + imphash: string; + }; + code_signature: { + trusted: boolean; + subject_name: string; + }; + malware_classification: MalwareClassification; + temp_file_path: string; + }; + host: Host; + dll?: DllFields[]; +}>; + +/** + * The status of the host + */ +export enum HostStatus { + /** + * Default state of the host when no host information is present or host information cannot + * be retrieved. e.g. API error + */ + ERROR = 'error', + + /** + * Host is online as indicated by its checkin status during the last checkin window + */ + ONLINE = 'online', + + /** + * Host is offline as indicated by its checkin status during the last checkin window + */ + OFFLINE = 'offline', +} + +export type HostInfo = Immutable<{ + metadata: HostMetadata; + host_status: HostStatus; +}>; + +export type HostMetadata = Immutable<{ + '@timestamp': number; + event: { + created: number; + }; + elastic: { + agent: { + id: string; + }; + }; + endpoint: { + policy: { + id: string; + }; + }; + agent: { + id: string; + version: string; + }; + host: Host; +}>; + +export interface LegacyEndpointEvent { + '@timestamp': number; + endgame: { + pid?: number; + ppid?: number; + event_type_full?: string; + event_subtype_full?: string; + event_timestamp?: number; + event_type?: number; + unique_pid: number; + unique_ppid?: number; + machine_id?: string; + process_name?: string; + process_path?: string; + timestamp_utc?: string; + serial_event_id?: number; + }; + agent: { + id: string; + type: string; + version: string; + }; + process?: object; + rule?: object; + user?: object; + event?: { + action?: string; + type?: string; + category?: string | string[]; + }; +} + +export interface EndpointEvent { + '@timestamp': number; + agent: { + id: string; + version: string; + type: string; + }; + ecs: { + version: string; + }; + event: { + category: string | string[]; + type: string | string[]; + id: string; + kind: string; + }; + host: { + id: string; + hostname: string; + ip: string[]; + mac: string[]; + os: HostOS; + }; + process: { + entity_id: string; + name: string; + parent?: { + entity_id: string; + name?: string; + }; + }; +} + +export type ResolverEvent = EndpointEvent | LegacyEndpointEvent; + +/** + * Takes a @kbn/config-schema 'schema' type and returns a type that represents valid inputs. + * Similar to `TypeOf`, but allows strings as input for `schema.number()` (which is inline + * with the behavior of the validator.) Also, for `schema.object`, when a value is a `schema.maybe` + * the key will be marked optional (via `?`) so that you can omit keys for optional values. + * + * Use this when creating a value that will be passed to the schema. + * e.g. + * ```ts + * const input: KbnConfigSchemaInputTypeOf = value + * schema.validate(input) // should be valid + * ``` + * Note that because the types coming from `@kbn/config-schema`'s schemas sometimes have deeply nested + * `Type` types, we process the result of `TypeOf` instead, as this will be consistent. + */ +export type KbnConfigSchemaInputTypeOf = T extends Record + ? KbnConfigSchemaInputObjectTypeOf< + T + > /** `schema.number()` accepts strings, so this type should accept them as well. */ + : number extends T + ? T | string + : T; + +/** + * Works like ObjectResultType, except that 'maybe' schema will create an optional key. + * This allows us to avoid passing 'maybeKey: undefined' when constructing such an object. + * + * Instead of using this directly, use `InputTypeOf`. + */ +type KbnConfigSchemaInputObjectTypeOf

> = { + /** Use ? to make the field optional if the prop accepts undefined. + * This allows us to avoid writing `field: undefined` for optional fields. + */ + [K in Exclude>]?: KbnConfigSchemaInputTypeOf< + P[K] + >; +} & + { [K in keyof KbnConfigSchemaNonOptionalProps

]: KbnConfigSchemaInputTypeOf }; + +/** + * Takes the props of a schema.object type, and returns a version that excludes + * optional values. Used by `InputObjectTypeOf`. + * + * Instead of using this directly, use `InputTypeOf`. + */ +type KbnConfigSchemaNonOptionalProps> = Pick< + Props, + { + [Key in keyof Props]: undefined extends Props[Key] + ? never + : null extends Props[Key] + ? never + : Key; + }[keyof Props] +>; + +/** + * Endpoint Policy configuration + */ +export interface PolicyConfig { + windows: { + events: { + dll_and_driver_load: boolean; + dns: boolean; + file: boolean; + network: boolean; + process: boolean; + registry: boolean; + security: boolean; + }; + malware: MalwareFields; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; + mac: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + malware: MalwareFields; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; + linux: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; +} + +/** + * The set of Policy configuration settings that are show/edited via the UI + */ +export interface UIPolicyConfig { + /** + * Windows-specific policy configuration that is supported via the UI + */ + windows: Pick; + /** + * Mac-specific policy configuration that is supported via the UI + */ + mac: Pick; + /** + * Linux-specific policy configuration that is supported via the UI + */ + linux: Pick; +} + +interface PolicyConfigAdvancedOptions { + elasticsearch: { + indices: { + control: string; + event: string; + logging: string; + }; + kernel: { + connect: boolean; + process: boolean; + }; + }; +} + +/** Policy: Malware protection fields */ +export interface MalwareFields { + mode: ProtectionModes; +} + +/** Policy protection mode options */ +export enum ProtectionModes { + detect = 'detect', + prevent = 'prevent', + preventNotify = 'preventNotify', + off = 'off', +} + +/** + * Endpoint Policy data, which extends Ingest's `Datasource` type + */ +export type PolicyData = Datasource & NewPolicyData; + +/** + * New policy data. Used when updating the policy record via ingest APIs + */ +export type NewPolicyData = NewDatasource & { + inputs: [ + { + type: 'endpoint'; + enabled: boolean; + streams: []; + config: { + policy: { + value: PolicyConfig; + }; + }; + } + ]; +}; + +/** + * the possible status for actions, configurations and overall Policy Response + */ +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +/** + * The name of actions that can be applied during the processing of a policy + */ +type HostPolicyActionName = + | 'download_model' + | 'ingest_events_config' + | 'workflow' + | 'configure_elasticsearch_connection' + | 'configure_kernel' + | 'configure_logging' + | 'configure_malware' + | 'connect_kernel' + | 'detect_file_open_events' + | 'detect_file_write_events' + | 'detect_image_load_events' + | 'detect_process_events' + | 'download_global_artifacts' + | 'load_config' + | 'load_malware_model' + | 'read_elasticsearch_config' + | 'read_events_config' + | 'read_kernel_config' + | 'read_logging_config' + | 'read_malware_config' + | string; + +/** + * Host Policy Response Applied Action + */ +export interface HostPolicyResponseAppliedAction { + name: HostPolicyActionName; + status: HostPolicyResponseActionStatus; + message: string; +} + +export type HostPolicyResponseConfiguration = HostPolicyResponse['endpoint']['policy']['applied']['response']['configurations']; + +interface HostPolicyResponseConfigurationStatus { + status: HostPolicyResponseActionStatus; + concerned_actions: HostPolicyActionName[]; +} + +/** + * Host Policy Response Applied Artifact + */ +interface HostPolicyResponseAppliedArtifact { + name: string; + sha256: string; +} + +/** + * Information about the applying of a policy to a given host + */ +export interface HostPolicyResponse { + '@timestamp': number; + elastic: { + agent: { + id: string; + }; + }; + ecs: { + version: string; + }; + host: { + id: string; + }; + event: { + created: number; + kind: string; + id: string; + category: string; + type: string; + module: string; + action: string; + dataset: string; + }; + agent: { + version: string; + id: string; + }; + endpoint: { + policy: { + applied: { + version: string; + id: string; + status: HostPolicyResponseActionStatus; + actions: HostPolicyResponseAppliedAction[]; + response: { + configurations: { + malware: HostPolicyResponseConfigurationStatus; + events: HostPolicyResponseConfigurationStatus; + logging: HostPolicyResponseConfigurationStatus; + streaming: HostPolicyResponseConfigurationStatus; + }; + }; + artifacts: { + global: { + version: string; + identifiers: HostPolicyResponseAppliedArtifact[]; + }; + user: { + version: string; + identifiers: HostPolicyResponseAppliedArtifact[]; + }; + }; + }; + }; + }; +} + +/** + * REST API response for retrieving a host's Policy Response status + */ +export interface GetHostPolicyResponse { + policy_response: HostPolicyResponse; +} diff --git a/x-pack/plugins/siem/common/endpoint_alerts/alert_constants.ts b/x-pack/plugins/security_solution/common/endpoint_alerts/alert_constants.ts similarity index 100% rename from x-pack/plugins/siem/common/endpoint_alerts/alert_constants.ts rename to x-pack/plugins/security_solution/common/endpoint_alerts/alert_constants.ts diff --git a/x-pack/plugins/siem/common/endpoint_alerts/schema/alert_index.ts b/x-pack/plugins/security_solution/common/endpoint_alerts/schema/alert_index.ts similarity index 84% rename from x-pack/plugins/siem/common/endpoint_alerts/schema/alert_index.ts rename to x-pack/plugins/security_solution/common/endpoint_alerts/schema/alert_index.ts index 6794eea20066b..bf57a366f341d 100644 --- a/x-pack/plugins/siem/common/endpoint_alerts/schema/alert_index.ts +++ b/x-pack/plugins/security_solution/common/endpoint_alerts/schema/alert_index.ts @@ -47,7 +47,7 @@ export const alertingIndexGetQuerySchema = schema.object( try { decode(value); } catch (err) { - return i18n.translate('xpack.siem.endpoint.alerts.errors.bad_rison', { + return i18n.translate('xpack.securitySolution.endpoint.alerts.errors.bad_rison', { defaultMessage: 'must be a valid rison-encoded string', }); } @@ -62,7 +62,7 @@ export const alertingIndexGetQuerySchema = schema.object( try { decode(value); } catch (err) { - return i18n.translate('xpack.siem.endpoint.alerts.errors.bad_rison', { + return i18n.translate('xpack.securitySolution.endpoint.alerts.errors.bad_rison', { defaultMessage: 'must be a valid rison-encoded string', }); } @@ -77,7 +77,7 @@ export const alertingIndexGetQuerySchema = schema.object( try { decode(value); } catch (err) { - return i18n.translate('xpack.siem.endpoint.alerts.errors.bad_rison', { + return i18n.translate('xpack.securitySolution.endpoint.alerts.errors.bad_rison', { defaultMessage: 'must be a valid rison-encoded string', }); } @@ -89,7 +89,7 @@ export const alertingIndexGetQuerySchema = schema.object( validate(value) { if (value.after !== undefined && value.page_index !== undefined) { return i18n.translate( - 'xpack.siem.endpoint.alerts.errors.page_index_cannot_be_used_with_after', + 'xpack.securitySolution.endpoint.alerts.errors.page_index_cannot_be_used_with_after', { defaultMessage: '[page_index] cannot be used with [after]', } @@ -97,7 +97,7 @@ export const alertingIndexGetQuerySchema = schema.object( } if (value.before !== undefined && value.page_index !== undefined) { return i18n.translate( - 'xpack.siem.endpoint.alerts.errors.page_index_cannot_be_used_with_before', + 'xpack.securitySolution.endpoint.alerts.errors.page_index_cannot_be_used_with_before', { defaultMessage: '[page_index] cannot be used with [before]', } @@ -105,7 +105,7 @@ export const alertingIndexGetQuerySchema = schema.object( } if (value.before !== undefined && value.after !== undefined) { return i18n.translate( - 'xpack.siem.endpoint.alerts.errors.before_cannot_be_used_with_after', + 'xpack.securitySolution.endpoint.alerts.errors.before_cannot_be_used_with_after', { defaultMessage: '[before] cannot be used with [after]', } diff --git a/x-pack/plugins/siem/common/endpoint_alerts/schema/index_pattern.ts b/x-pack/plugins/security_solution/common/endpoint_alerts/schema/index_pattern.ts similarity index 100% rename from x-pack/plugins/siem/common/endpoint_alerts/schema/index_pattern.ts rename to x-pack/plugins/security_solution/common/endpoint_alerts/schema/index_pattern.ts diff --git a/x-pack/plugins/siem/common/endpoint_alerts/types.ts b/x-pack/plugins/security_solution/common/endpoint_alerts/types.ts similarity index 100% rename from x-pack/plugins/siem/common/endpoint_alerts/types.ts rename to x-pack/plugins/security_solution/common/endpoint_alerts/types.ts diff --git a/x-pack/plugins/siem/common/exact_check.test.ts b/x-pack/plugins/security_solution/common/exact_check.test.ts similarity index 100% rename from x-pack/plugins/siem/common/exact_check.test.ts rename to x-pack/plugins/security_solution/common/exact_check.test.ts diff --git a/x-pack/plugins/siem/common/exact_check.ts b/x-pack/plugins/security_solution/common/exact_check.ts similarity index 100% rename from x-pack/plugins/siem/common/exact_check.ts rename to x-pack/plugins/security_solution/common/exact_check.ts diff --git a/x-pack/plugins/siem/common/format_errors.test.ts b/x-pack/plugins/security_solution/common/format_errors.test.ts similarity index 100% rename from x-pack/plugins/siem/common/format_errors.test.ts rename to x-pack/plugins/security_solution/common/format_errors.test.ts diff --git a/x-pack/plugins/siem/common/format_errors.ts b/x-pack/plugins/security_solution/common/format_errors.ts similarity index 100% rename from x-pack/plugins/siem/common/format_errors.ts rename to x-pack/plugins/security_solution/common/format_errors.ts diff --git a/x-pack/plugins/siem/common/graphql/root/index.ts b/x-pack/plugins/security_solution/common/graphql/root/index.ts similarity index 100% rename from x-pack/plugins/siem/common/graphql/root/index.ts rename to x-pack/plugins/security_solution/common/graphql/root/index.ts diff --git a/x-pack/plugins/siem/common/graphql/root/schema.gql.ts b/x-pack/plugins/security_solution/common/graphql/root/schema.gql.ts similarity index 100% rename from x-pack/plugins/siem/common/graphql/root/schema.gql.ts rename to x-pack/plugins/security_solution/common/graphql/root/schema.gql.ts diff --git a/x-pack/plugins/siem/common/graphql/shared/index.ts b/x-pack/plugins/security_solution/common/graphql/shared/index.ts similarity index 100% rename from x-pack/plugins/siem/common/graphql/shared/index.ts rename to x-pack/plugins/security_solution/common/graphql/shared/index.ts diff --git a/x-pack/plugins/siem/common/graphql/shared/schema.gql.ts b/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts similarity index 100% rename from x-pack/plugins/siem/common/graphql/shared/schema.gql.ts rename to x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts diff --git a/x-pack/plugins/siem/common/machine_learning/empty_ml_capabilities.ts b/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts similarity index 100% rename from x-pack/plugins/siem/common/machine_learning/empty_ml_capabilities.ts rename to x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts diff --git a/x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.test.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_admin_permissions.test.ts similarity index 100% rename from x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.test.ts rename to x-pack/plugins/security_solution/common/machine_learning/has_ml_admin_permissions.test.ts diff --git a/x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_admin_permissions.ts similarity index 100% rename from x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.ts rename to x-pack/plugins/security_solution/common/machine_learning/has_ml_admin_permissions.ts diff --git a/x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.test.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_user_permissions.test.ts similarity index 100% rename from x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.test.ts rename to x-pack/plugins/security_solution/common/machine_learning/has_ml_user_permissions.test.ts diff --git a/x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_user_permissions.ts similarity index 100% rename from x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.ts rename to x-pack/plugins/security_solution/common/machine_learning/has_ml_user_permissions.ts diff --git a/x-pack/plugins/siem/common/machine_learning/helpers.test.ts b/x-pack/plugins/security_solution/common/machine_learning/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/common/machine_learning/helpers.test.ts rename to x-pack/plugins/security_solution/common/machine_learning/helpers.test.ts diff --git a/x-pack/plugins/siem/common/machine_learning/helpers.ts b/x-pack/plugins/security_solution/common/machine_learning/helpers.ts similarity index 100% rename from x-pack/plugins/siem/common/machine_learning/helpers.ts rename to x-pack/plugins/security_solution/common/machine_learning/helpers.ts diff --git a/x-pack/plugins/siem/common/test_utils.ts b/x-pack/plugins/security_solution/common/test_utils.ts similarity index 100% rename from x-pack/plugins/siem/common/test_utils.ts rename to x-pack/plugins/security_solution/common/test_utils.ts diff --git a/x-pack/plugins/siem/common/typed_json.ts b/x-pack/plugins/security_solution/common/typed_json.ts similarity index 98% rename from x-pack/plugins/siem/common/typed_json.ts rename to x-pack/plugins/security_solution/common/typed_json.ts index 62e7319e091cb..61c1093002192 100644 --- a/x-pack/plugins/siem/common/typed_json.ts +++ b/x-pack/plugins/security_solution/common/typed_json.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { JsonObject } from '../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../src/plugins/kibana_utils/common'; export type ESQuery = ESRangeQuery | ESQueryStringQuery | ESMatchQuery | ESTermQuery | JsonObject; diff --git a/x-pack/plugins/siem/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts similarity index 100% rename from x-pack/plugins/siem/common/types/timeline/index.ts rename to x-pack/plugins/security_solution/common/types/timeline/index.ts diff --git a/x-pack/plugins/siem/common/types/timeline/note/index.ts b/x-pack/plugins/security_solution/common/types/timeline/note/index.ts similarity index 100% rename from x-pack/plugins/siem/common/types/timeline/note/index.ts rename to x-pack/plugins/security_solution/common/types/timeline/note/index.ts diff --git a/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts b/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts similarity index 100% rename from x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts rename to x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts similarity index 100% rename from x-pack/plugins/siem/common/utility_types.ts rename to x-pack/plugins/security_solution/common/utility_types.ts diff --git a/x-pack/plugins/siem/cypress/.eslintrc.json b/x-pack/plugins/security_solution/cypress/.eslintrc.json similarity index 100% rename from x-pack/plugins/siem/cypress/.eslintrc.json rename to x-pack/plugins/security_solution/cypress/.eslintrc.json diff --git a/x-pack/plugins/siem/cypress/.gitignore b/x-pack/plugins/security_solution/cypress/.gitignore similarity index 100% rename from x-pack/plugins/siem/cypress/.gitignore rename to x-pack/plugins/security_solution/cypress/.gitignore diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md new file mode 100644 index 0000000000000..ad3af2aaa4e8a --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -0,0 +1,355 @@ +# Cypress Tests + +The `security_solution/cypress` directory contains end to end tests, (plus a few tests +that rely on mocked API calls), that execute via [Cypress](https://www.cypress.io/). + +Cypress tests may be run against: + +- A local Kibana instance, interactively or via the command line. Credentials +are specified via `kibana.dev.yml` or environment variables. +- A remote Elastic Cloud instance (override `baseUrl`), interactively or via +the command line. Again, credentials are specified via `kibana.dev.yml` or +environment variables. +- As part of CI (override `baseUrl` and pass credentials via the +`CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables), via command line. + +At present, Cypress tests are only executed manually. They are **not** yet +integrated in the Kibana CI infrastructure, and therefore do **not** run +automatically when you submit a PR. + +## Smoke Tests + +Smoke Tests are located in `security_solution/cypress/integration/smoke_tests` + +## Structure + +### Tasks + +_Tasks_ are functions that my be re-used across tests. Inside the _tasks_ folder there are some other folders that represents +the page to which we will perform the actions. For each folder we are going to create a file for each one of the sections that + has the page. + +i.e. +- tasks + - hosts + - events.ts + +### Screens + +In _screens_ folder we are going to find all the elements we want to interact in our tests. Inside _screens_ fonder there +are some other folders that represents the page that contains the elements the tests are going to interact with. For each +folder we are going to create a file for each one of the sections that the page has. + +i.e. +- tasks + - hosts + - events.ts + +## Mock Data + +We prefer not to mock API responses in most of our smoke tests, but sometimes +it's necessary because a test must assert that a specific value is rendered, +and it's not possible to derive that value based on the data in the +environment where tests are running. + +Mocked responses API from the server are located in `security_solution/cypress/fixtures`. + +## Speeding up test execution time + +Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be +taken into consideration until another solution is implemented: + +- Don't refresh the page for every test to clean the state of it. +- Instead, group the tests that are similar in different contexts. +- For every context login only once, clean the state between tests if needed without re-loading the page. +- All tests in a spec file must be order-independent. + - If you need to reload the page to make the tests order-independent, consider to create a new context. + +Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. + +## Authentication + +When running tests, there are two ways to specify the credentials used to +authenticate with Kibana: + +- Via `kibana.dev.yml` (recommended for developers) +- Via the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables (recommended for CI), or when testing a remote Kibana +instance, e.g. in Elastic Cloud. + +Note: Tests that use the `login()` test helper function for authentication will +automatically use the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables when they are defined, and fall back to the values in +`config/kibana.dev.yml` when they are unset. + +### Content Security Policy (CSP) Settings + +Your local or cloud Kibana server must have the `csp.strict: false` setting +configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below: + +```yaml +csp.strict: false +``` + +The above setting is required to prevent the _Please upgrade +your browser_ / _This Kibana installation has strict security requirements +enabled that your current browser does not meet._ warning that's displayed for +unsupported user agents, like the one reported by Cypress when running tests. + +### Example `kibana.dev.yml` + +If you're a developer running tests interactively or on the command line, the +easiset way to specify the credentials used for authentication is to update + `kibana.dev.yml` per the following example: + +```yaml +csp.strict: false +elasticsearch: + username: 'elastic' + password: '' + hosts: ['https://:9200'] +``` + +## Running (Headless) Tests on the Command Line as a Jenkins execution (The preferred way) + +To run (headless) tests as a Jenkins execution. + +1. First bootstrap kibana changes from the Kibana root directory: + +```sh +yarn kbn bootstrap +``` + +2. Launch Cypress command line test runner: + +```sh +cd x-pack/plugins/security_solution +yarn cypress:run-as-ci +``` + +Note that with this type of execution you don't need to have running a kibana and elasticsearch instance. This is because + the command, as it would happen in the CI, will launch the instances. The elasticsearch instance will be fed data + found in: `x-pack/test/security_solution_cypress/es_archives` + +As in this case we want to mimic a CI execution we want to execute the tests with the same set of data, this is why +in this case does not make sense to override Cypress environment variables. + +### Test data + +As mentioned above, when running the tests as Jenkins the tests are populated with data ("archives") found in: `x-pack/test/security_solution_cypress/es_archives`. + +By default, each test is populated with some base data: an empty kibana index and a set of auditbeat data (the `empty_kibana` and `auditbeat` archives, respectively). This is usually enough to cover most of the scenarios that we are testing. + +#### Running tests with additional archives + +When the base data is insufficient, one can specify additional archives. Use `esArchiverLoad` to load the necessary archive, and `esArchiverUnload` to remove the archive from elasticsearch: + +```typescript +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; + +describe('This are going to be a set of tests', () => { + before(() => { + esArchiverLoad('name_of_the_data_set_you_want_to_load'); + }); + + after(() => { + esArchiverUnload('name_of_the_data_set_you_want_to_unload'); + }); + + it('Going to test something awesome', () => { + hereGoesYourAwesomeTestcode + }); +}); + +``` + +Note that loading and unloading data take a significant amount of time, so try to minimize their use. + +### Current archives + +The current archives can be found in `x-pack/test/security_solution_cypress/es_archives/`. + +- auditbeat + - Auditbeat data generated in Sep, 2019 with the following hosts present: + - suricata-iowa + - siem-kibana + - siem-es + - jessie +- closed_alerts + - Set of data with 108 closed alerts linked to "Alerts test" custom rule. +- custome_rules + - Set if data with just 4 custom activated rules. +- empty_kibana + - Empty kibana board. +- prebuilt_rules_loaded + - Elastic prebuilt loaded rules and deactivated. +- alerts + - Set of data with 108 opened alerts linked to "Alerts test" custom rule. + +### How to generate a new archive + +We are using es_archiver in order to manage the data that our Cypress tests needs. + +1. Setup if possible a clean instance of kibana and elasticsearch (if not, possible please try to clean the data that you are going to generate). +2. With the kibana and elasticsearch instance up and running, create the data that you need for your test. +3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution` + +```sh +node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +``` + +Example: +```sh +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +``` + +Note that the command is going to create the folder if does not exist in the directory with the imported data. + + +## Running Tests Interactively + +Use the Cypress interactive test runner to develop and debug specific tests +by adding a `.only` to the test you're developing, or click on a specific +spec in the interactive test runner to run just the tests in that spec. + +To run and debug tests in interactively via the Cypress test runner: + +1. Disable CSP on the local or remote Kibana instance, as described in the +_Content Security Policy (CSP) Settings_ section above. + +2. To specify the credentials required for authentication, configure +`config/kibana.dev.yml`, as described in the _Server and Authentication +Requirements_ section above, or specify them via environment variables +as described later in this section. + +3. Start a local instance of the Kibana development server (only if testing against a +local host): + +```sh +yarn start --no-base-path +``` + +4. Launch the Cypress interactive test runner via one of the following options: + +- To run tests interactively against the default (local) host specified by +`baseUrl`, as configured in `plugins/security_solution/cypress.json`: + +```sh +cd x-pack/plugins/security_solution +yarn cypress:open +``` + +- To (optionally) run tests interactively against a different host, pass the +`CYPRESS_baseUrl` environment variable on the command line when launching the +test runner, as shown in the following example: + +```sh +cd x-pack/plugins/security_solution +CYPRESS_baseUrl=http://localhost:5601 yarn cypress:open +``` + +- To (optionally) override username and password via environment variables when +running tests interactively: + +```sh +cd x-pack/plugins/security_solution +CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:open +``` + +5. Click the `Run all specs` button in the Cypress test runner (after adding +a `.only` to an `it` or `describe` block). + +## Running (Headless) Tests on the Command Line + +To run (headless) tests on the command line: + +1. Disable CSP on the local or remote Kibana instance, as described in the +_Content Security Policy (CSP) Settings_ section above. + +2. To specify the credentials required for authentication, configure +`config/kibana.dev.yml`, as described in the _Server and Authentication +Requirements_ section above, or specify them via environment variables +as described later in this section. + +3. Start a local instance of the Kibana development server (only if testing against a +local host): + +```sh +yarn start --no-base-path +``` + +4. Launch the Cypress command line test runner via one of the following options: + +- To run tests on the command line against the default (local) host specified by +`baseUrl`, as configured in `plugins/security_solution/cypress.json`: + +```sh +cd x-pack/plugins/security_solution +yarn cypress:run +``` + +- To (optionally) run tests on the command line against a different host, pass +`CYPRESS_baseUrl` as an environment variable on the command line, as shown in +the following example: + +```sh +cd x-pack/plugins/security_solution +CYPRESS_baseUrl=http://localhost:5601 yarn cypress:run +``` + +- To (optionally) override username and password via environment variables when +running via the command line: + +```sh +cd x-pack/plugins/security_solution +CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run +``` + +## Reporting + +When Cypress tests are run on the command line via `yarn cypress:run`, +reporting artifacts are generated under the `target` directory in the root +of the Kibana, as detailed for each artifact type in the sections below. + +### HTML Reports + +An HTML report (e.g. for email notifications) is output to: + +``` +target/kibana-security-solution/cypress/results/output.html +``` + +### Screenshots + +Screenshots of failed tests are output to: + +``` +target/kibana-security-solution/cypress/screenshots +``` + +### `junit` Reports + +The Kibana CI process reports `junit` test results from the `target/junit` directory. + +Cypress `junit` reports are generated in `target/kibana-security-solution/cypress/results` +and copied to the `target/junit` directory. + +### Videos (optional) + +Videos are disabled by default, but can optionally be enabled by setting the +`CYPRESS_video=true` environment variable: + +``` +CYPRESS_video=true yarn cypress:run +``` + +Videos are (optionally) output to: + +``` +target/kibana-security-solution/cypress/videos +``` + +## Linting + +Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) diff --git a/x-pack/plugins/security_solution/cypress/cypress.json b/x-pack/plugins/security_solution/cypress/cypress.json new file mode 100644 index 0000000000000..b097b0432e75d --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost:5601", + "defaultCommandTimeout": 120000, + "screenshotsFolder": "../../../target/kibana-security-solution/cypress/screenshots", + "trashAssetsBeforeRuns": false, + "video": false, + "videosFolder": "../../../target/kibana-security-solution/cypress/videos" +} diff --git a/x-pack/plugins/siem/cypress/fixtures/overview.json b/x-pack/plugins/security_solution/cypress/fixtures/overview.json similarity index 100% rename from x-pack/plugins/siem/cypress/fixtures/overview.json rename to x-pack/plugins/security_solution/cypress/fixtures/overview.json diff --git a/x-pack/plugins/siem/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/cases.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/cases.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detections.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/detections.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/detections.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detections_timeline.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/detections_timeline.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/fields_browser.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/inspect.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts new file mode 100644 index 0000000000000..e96c1fe691966 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -0,0 +1,203 @@ +/* + * 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 { KQL_INPUT } from '../screens/siem_header'; + +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { + mlHostMultiHostKqlQuery, + mlHostMultiHostNullKqlQuery, + mlHostSingleHostKqlQuery, + mlHostSingleHostKqlQueryVariable, + mlHostSingleHostNullKqlQuery, + mlHostVariableHostKqlQuery, + mlHostVariableHostNullKqlQuery, + mlNetworkKqlQuery, + mlNetworkMultipleIpKqlQuery, + mlNetworkMultipleIpNullKqlQuery, + mlNetworkNullKqlQuery, + mlNetworkSingleIpKqlQuery, + mlNetworkSingleIpNullKqlQuery, +} from '../urls/ml_conditional_links'; + +describe('ml conditional links', () => { + it('sets the KQL from a single IP with a value for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '(process.name: "conhost.exe" or process.name: "sc.exe")' + ); + }); + + it('sets the KQL from a multiple IPs with a null for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))' + ); + }); + + it('sets the KQL from a multiple IPs with a value for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))' + ); + }); + + it('sets the KQL from a $ip$ with a value for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '(process.name: "conhost.exe" or process.name: "sc.exe")' + ); + }); + + it('sets the KQL from a single host name with a value for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '(process.name: "conhost.exe" or process.name: "sc.exe")' + ); + }); + + it('sets the KQL from a multiple host names with null for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '(host.name: "siem-windows" or host.name: "siem-suricata")' + ); + }); + + it('sets the KQL from a multiple host names with a value for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))' + ); + }); + + it('sets the KQL from a undefined/null host name but with a value for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); + cy.get(KQL_INPUT).should( + 'have.attr', + 'value', + '(process.name: "conhost.exe" or process.name: "sc.exe")' + ); + }); + + it('redirects from a single IP with a null for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery); + cy.url().should( + 'include', + '/app/security#/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + ); + }); + + it('redirects from a single IP with a value for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); + cy.url().should( + 'include', + "/app/security#/network/ip/127.0.0.1/source?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + ); + }); + + it('redirects from a multiple IPs with a null for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); + cy.url().should( + 'include', + "app/security#/network/flows?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999))" + ); + }); + + it('redirects from a multiple IPs with a value for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); + cy.url().should( + 'include', + "/app/security#/network/flows?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + ); + }); + + it('redirects from a $ip$ with a null query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery); + cy.url().should( + 'include', + '/app/security#/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + ); + }); + + it('redirects from a $ip$ with a value for the query', () => { + loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); + cy.url().should( + 'include', + "/app/security#/network/flows?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + ); + }); + + it('redirects from a single host name with a null for the query', () => { + loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery); + cy.url().should( + 'include', + '/app/security#/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + ); + }); + + it('redirects from a host name with a variable in the query', () => { + loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable); + cy.url().should( + 'include', + '/app/security#/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + ); + }); + + it('redirects from a single host name with a value for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); + cy.url().should( + 'include', + "/app/security#/hosts/siem-windows/anomalies?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + ); + }); + + it('redirects from a multiple host names with null for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); + cy.url().should( + 'include', + "/app/security#/hosts/anomalies?query=(language:kuery,query:'(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + ); + }); + + it('redirects from a multiple host names with a value for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); + cy.url().should( + 'include', + "/app/security#/hosts/anomalies?query=(language:kuery,query:'(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + ); + }); + + it('redirects from a undefined/null host name with a null for the KQL', () => { + loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery); + cy.url().should( + 'include', + '/app/security#/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + ); + }); + + it('redirects from a undefined/null host name but with a value for query', () => { + loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); + cy.url().should( + 'include', + "/app/security#/hosts/anomalies?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + ); + }); +}); diff --git a/x-pack/plugins/siem/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts similarity index 79% rename from x-pack/plugins/siem/cypress/integration/navigation.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts index bebd5f7d679cf..a9e5a848d54a8 100644 --- a/x-pack/plugins/siem/cypress/integration/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts @@ -16,26 +16,26 @@ describe('top-level navigation common to all pages in the SIEM app', () => { }); it('navigates to the Overview page', () => { navigateFromHeaderTo(OVERVIEW); - cy.url().should('include', '/siem#/overview'); + cy.url().should('include', '/security#/overview'); }); it('navigates to the Hosts page', () => { navigateFromHeaderTo(HOSTS); - cy.url().should('include', '/siem#/hosts'); + cy.url().should('include', '/security#/hosts'); }); it('navigates to the Network page', () => { navigateFromHeaderTo(NETWORK); - cy.url().should('include', '/siem#/network'); + cy.url().should('include', '/security#/network'); }); it('navigates to the Detections page', () => { navigateFromHeaderTo(DETECTIONS); - cy.url().should('include', '/siem#/detections'); + cy.url().should('include', '/security#/detections'); }); it('navigates to the Timelines page', () => { navigateFromHeaderTo(TIMELINES); - cy.url().should('include', '/siem#/timelines'); + cy.url().should('include', '/security#/timelines'); }); }); diff --git a/x-pack/plugins/siem/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/overview.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/overview.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/pagination.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/signal_detection_rules.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_custom.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_custom.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_export.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_export.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_ml.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_ml.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_prebuilt.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_prebuilt.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/timeline_data_providers.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts similarity index 100% rename from x-pack/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts similarity index 98% rename from x-pack/plugins/siem/cypress/integration/url_state.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 5625e1812f696..cf69a83dbf951 100644 --- a/x-pack/plugins/siem/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -255,8 +255,8 @@ describe('url state', () => { expect(matched).to.have.lengthOf(1); closeTimeline(); cy.visit('/app/kibana'); - cy.visit(`/app/siem#/overview?timeline\=(id:'${newTimelineId}',isOpen:!t)`); - cy.contains('a', 'SIEM'); + cy.visit(`/app/security#/overview?timeline\=(id:'${newTimelineId}',isOpen:!t)`); + cy.contains('a', 'Security'); cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).invoke('text').should('not.equal', 'Updating'); cy.get(TIMELINE_TITLE).should('have.attr', 'value', timelineName); }); diff --git a/x-pack/plugins/siem/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts similarity index 100% rename from x-pack/plugins/siem/cypress/objects/case.ts rename to x-pack/plugins/security_solution/cypress/objects/case.ts diff --git a/x-pack/plugins/siem/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts similarity index 100% rename from x-pack/plugins/siem/cypress/objects/rule.ts rename to x-pack/plugins/security_solution/cypress/objects/rule.ts diff --git a/x-pack/plugins/siem/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts similarity index 100% rename from x-pack/plugins/siem/cypress/objects/timeline.ts rename to x-pack/plugins/security_solution/cypress/objects/timeline.ts diff --git a/x-pack/plugins/siem/cypress/plugins/index.js b/x-pack/plugins/security_solution/cypress/plugins/index.js similarity index 100% rename from x-pack/plugins/siem/cypress/plugins/index.js rename to x-pack/plugins/security_solution/cypress/plugins/index.js diff --git a/x-pack/plugins/security_solution/cypress/reporter_config.json b/x-pack/plugins/security_solution/cypress/reporter_config.json new file mode 100644 index 0000000000000..b72a5cf077c6a --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/reporter_config.json @@ -0,0 +1,10 @@ +{ + "reporterEnabled": "mochawesome, mocha-junit-reporter", + "reporterOptions": { + "html": false, + "json": true, + "mochaFile": "../../../target/kibana-security-solution/cypress/results/TEST-security-solution-cypress-[hash].xml", + "overwrite": false, + "reportDir": "../../../target/kibana-security-solution/cypress/results" + } +} diff --git a/x-pack/plugins/siem/cypress/screens/alert_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alert_detection_rules.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/alert_detection_rules.ts rename to x-pack/plugins/security_solution/cypress/screens/alert_detection_rules.ts diff --git a/x-pack/plugins/siem/cypress/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/all_cases.ts rename to x-pack/plugins/security_solution/cypress/screens/all_cases.ts diff --git a/x-pack/plugins/siem/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/case_details.ts rename to x-pack/plugins/security_solution/cypress/screens/case_details.ts diff --git a/x-pack/plugins/siem/cypress/screens/configure_cases.ts b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/configure_cases.ts rename to x-pack/plugins/security_solution/cypress/screens/configure_cases.ts diff --git a/x-pack/plugins/siem/cypress/screens/create_new_case.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/create_new_case.ts rename to x-pack/plugins/security_solution/cypress/screens/create_new_case.ts diff --git a/x-pack/plugins/siem/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/create_new_rule.ts rename to x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts diff --git a/x-pack/plugins/siem/cypress/screens/date_picker.ts b/x-pack/plugins/security_solution/cypress/screens/date_picker.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/date_picker.ts rename to x-pack/plugins/security_solution/cypress/screens/date_picker.ts diff --git a/x-pack/plugins/siem/cypress/screens/detections.ts b/x-pack/plugins/security_solution/cypress/screens/detections.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/detections.ts rename to x-pack/plugins/security_solution/cypress/screens/detections.ts diff --git a/x-pack/plugins/siem/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/fields_browser.ts rename to x-pack/plugins/security_solution/cypress/screens/fields_browser.ts diff --git a/x-pack/plugins/siem/cypress/screens/hosts/all_hosts.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/hosts/all_hosts.ts rename to x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts diff --git a/x-pack/plugins/siem/cypress/screens/hosts/authentications.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/authentications.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/hosts/authentications.ts rename to x-pack/plugins/security_solution/cypress/screens/hosts/authentications.ts diff --git a/x-pack/plugins/siem/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/hosts/events.ts rename to x-pack/plugins/security_solution/cypress/screens/hosts/events.ts diff --git a/x-pack/plugins/siem/cypress/screens/hosts/main.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/main.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/hosts/main.ts rename to x-pack/plugins/security_solution/cypress/screens/hosts/main.ts diff --git a/x-pack/plugins/siem/cypress/screens/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/hosts/uncommon_processes.ts rename to x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts diff --git a/x-pack/plugins/siem/cypress/screens/inspect.ts b/x-pack/plugins/security_solution/cypress/screens/inspect.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/inspect.ts rename to x-pack/plugins/security_solution/cypress/screens/inspect.ts diff --git a/x-pack/plugins/siem/cypress/screens/network/flows.ts b/x-pack/plugins/security_solution/cypress/screens/network/flows.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/network/flows.ts rename to x-pack/plugins/security_solution/cypress/screens/network/flows.ts diff --git a/x-pack/plugins/siem/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/overview.ts rename to x-pack/plugins/security_solution/cypress/screens/overview.ts diff --git a/x-pack/plugins/siem/cypress/screens/pagination.ts b/x-pack/plugins/security_solution/cypress/screens/pagination.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/pagination.ts rename to x-pack/plugins/security_solution/cypress/screens/pagination.ts diff --git a/x-pack/plugins/siem/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/rule_details.ts rename to x-pack/plugins/security_solution/cypress/screens/rule_details.ts diff --git a/x-pack/plugins/siem/cypress/screens/siem_header.ts b/x-pack/plugins/security_solution/cypress/screens/siem_header.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/siem_header.ts rename to x-pack/plugins/security_solution/cypress/screens/siem_header.ts diff --git a/x-pack/plugins/siem/cypress/screens/siem_main.ts b/x-pack/plugins/security_solution/cypress/screens/siem_main.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/siem_main.ts rename to x-pack/plugins/security_solution/cypress/screens/siem_main.ts diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/timeline.ts rename to x-pack/plugins/security_solution/cypress/screens/timeline.ts diff --git a/x-pack/plugins/siem/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js similarity index 94% rename from x-pack/plugins/siem/cypress/support/commands.js rename to x-pack/plugins/security_solution/cypress/support/commands.js index 4bc62da22aca7..d086981cb98f7 100644 --- a/x-pack/plugins/siem/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -37,5 +37,5 @@ Cypress.Commands.add('stubSIEMapi', function (dataFileName) { }); cy.server(); cy.fixture(dataFileName).as(`${dataFileName}JSON`); - cy.route('POST', 'api/siem/graphql', `@${dataFileName}JSON`); + cy.route('POST', 'api/solutions/security/graphql', `@${dataFileName}JSON`); }); diff --git a/x-pack/plugins/siem/cypress/support/index.d.ts b/x-pack/plugins/security_solution/cypress/support/index.d.ts similarity index 100% rename from x-pack/plugins/siem/cypress/support/index.d.ts rename to x-pack/plugins/security_solution/cypress/support/index.d.ts diff --git a/x-pack/plugins/siem/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js similarity index 100% rename from x-pack/plugins/siem/cypress/support/index.js rename to x-pack/plugins/security_solution/cypress/support/index.js diff --git a/x-pack/plugins/siem/cypress/tasks/alert_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alert_detection_rules.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/alert_detection_rules.ts rename to x-pack/plugins/security_solution/cypress/tasks/alert_detection_rules.ts diff --git a/x-pack/plugins/siem/cypress/tasks/all_cases.ts b/x-pack/plugins/security_solution/cypress/tasks/all_cases.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/all_cases.ts rename to x-pack/plugins/security_solution/cypress/tasks/all_cases.ts diff --git a/x-pack/plugins/siem/cypress/tasks/case_details.ts b/x-pack/plugins/security_solution/cypress/tasks/case_details.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/case_details.ts rename to x-pack/plugins/security_solution/cypress/tasks/case_details.ts diff --git a/x-pack/plugins/siem/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/common.ts rename to x-pack/plugins/security_solution/cypress/tasks/common.ts diff --git a/x-pack/plugins/siem/cypress/tasks/configure_cases.ts b/x-pack/plugins/security_solution/cypress/tasks/configure_cases.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/configure_cases.ts rename to x-pack/plugins/security_solution/cypress/tasks/configure_cases.ts diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/create_new_case.ts rename to x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/create_new_rule.ts rename to x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts diff --git a/x-pack/plugins/siem/cypress/tasks/date_picker.ts b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/date_picker.ts rename to x-pack/plugins/security_solution/cypress/tasks/date_picker.ts diff --git a/x-pack/plugins/siem/cypress/tasks/detections.ts b/x-pack/plugins/security_solution/cypress/tasks/detections.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/detections.ts rename to x-pack/plugins/security_solution/cypress/tasks/detections.ts diff --git a/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts new file mode 100644 index 0000000000000..5a09a2f753dc4 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts @@ -0,0 +1,45 @@ +/* + * 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 esArchiverLoadEmptyKibana = () => { + cy.exec( + `node ../../../scripts/es_archiver empty_kibana load empty--dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverLoad = (folder: string) => { + cy.exec( + `node ../../../scripts/es_archiver load ${folder} --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverUnload = (folder: string) => { + cy.exec( + `node ../../../scripts/es_archiver unload ${folder} --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverUnloadEmptyKibana = () => { + cy.exec( + `node ../../../scripts/es_archiver unload empty_kibana empty--dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverResetKibana = () => { + cy.exec( + `node ../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; diff --git a/x-pack/plugins/siem/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/fields_browser.ts rename to x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts diff --git a/x-pack/plugins/siem/cypress/tasks/hosts/all_hosts.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/all_hosts.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/hosts/all_hosts.ts rename to x-pack/plugins/security_solution/cypress/tasks/hosts/all_hosts.ts diff --git a/x-pack/plugins/siem/cypress/tasks/hosts/authentications.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/hosts/authentications.ts rename to x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts diff --git a/x-pack/plugins/siem/cypress/tasks/hosts/events.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/hosts/events.ts rename to x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts diff --git a/x-pack/plugins/siem/cypress/tasks/hosts/main.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/main.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/hosts/main.ts rename to x-pack/plugins/security_solution/cypress/tasks/hosts/main.ts diff --git a/x-pack/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts rename to x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts diff --git a/x-pack/plugins/siem/cypress/tasks/inspect.ts b/x-pack/plugins/security_solution/cypress/tasks/inspect.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/inspect.ts rename to x-pack/plugins/security_solution/cypress/tasks/inspect.ts diff --git a/x-pack/plugins/siem/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts similarity index 98% rename from x-pack/plugins/siem/cypress/tasks/login.ts rename to x-pack/plugins/security_solution/cypress/tasks/login.ts index 13580037b3d7c..4479c8d9d1fbe 100644 --- a/x-pack/plugins/siem/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -124,12 +124,12 @@ export const loginAndWaitForPage = (url: string) => { cy.visit( `${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))` ); - cy.contains('a', 'SIEM'); + cy.contains('a', 'Security'); }; export const loginAndWaitForPageWithoutDateRange = (url: string) => { login(); cy.viewport('macbook-15'); cy.visit(url); - cy.contains('a', 'SIEM', { timeout: 120000 }); + cy.contains('a', 'Security', { timeout: 120000 }); }; diff --git a/x-pack/plugins/siem/cypress/tasks/network/flows.ts b/x-pack/plugins/security_solution/cypress/tasks/network/flows.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/network/flows.ts rename to x-pack/plugins/security_solution/cypress/tasks/network/flows.ts diff --git a/x-pack/plugins/siem/cypress/tasks/overview.ts b/x-pack/plugins/security_solution/cypress/tasks/overview.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/overview.ts rename to x-pack/plugins/security_solution/cypress/tasks/overview.ts diff --git a/x-pack/plugins/siem/cypress/tasks/pagination.ts b/x-pack/plugins/security_solution/cypress/tasks/pagination.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/pagination.ts rename to x-pack/plugins/security_solution/cypress/tasks/pagination.ts diff --git a/x-pack/plugins/siem/cypress/tasks/siem_header.ts b/x-pack/plugins/security_solution/cypress/tasks/siem_header.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/siem_header.ts rename to x-pack/plugins/security_solution/cypress/tasks/siem_header.ts diff --git a/x-pack/plugins/siem/cypress/tasks/siem_main.ts b/x-pack/plugins/security_solution/cypress/tasks/siem_main.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/siem_main.ts rename to x-pack/plugins/security_solution/cypress/tasks/siem_main.ts diff --git a/x-pack/plugins/siem/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts similarity index 100% rename from x-pack/plugins/siem/cypress/tasks/timeline.ts rename to x-pack/plugins/security_solution/cypress/tasks/timeline.ts diff --git a/x-pack/plugins/siem/cypress/test_files/expected_rules_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson similarity index 100% rename from x-pack/plugins/siem/cypress/test_files/expected_rules_export.ndjson rename to x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson diff --git a/x-pack/plugins/siem/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json similarity index 100% rename from x-pack/plugins/siem/cypress/tsconfig.json rename to x-pack/plugins/security_solution/cypress/tsconfig.json diff --git a/x-pack/plugins/security_solution/cypress/urls/ml_conditional_links.ts b/x-pack/plugins/security_solution/cypress/urls/ml_conditional_links.ts new file mode 100644 index 0000000000000..cfa18099e5888 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/urls/ml_conditional_links.ts @@ -0,0 +1,76 @@ +/* + * 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. + */ + +/* + * These links are for different test scenarios that try and capture different drill downs into + * ml-network and ml-hosts and are of the flavor of testing: + * A filter being null: (query:!n) + * A filter being set with single values: query=(query:%27process.name%20:%20%22conhost.exe%22%27,language:kuery) + * A filter being set with multiple values: query=(query:%27process.name%20:%20%22conhost.exe,sc.exe%22%27,language:kuery) + * A filter containing variables not replaced: query=(query:%27process.name%20:%20%$process.name$%22%27,language:kuery) + * + * In different combination with: + * network not being set: $ip$ + * host not being set: $host.name$ + * ...or... + * network being set normally: 127.0.0.1 + * host being set normally: suricata-iowa + * ...or... + * network having multiple values: 127.0.0.1,127.0.0.2 + * host having multiple values: suricata-iowa,siem-windows + */ + +// Single IP with a null for the Query: +export const mlNetworkSingleIpNullKqlQuery = + "/app/security#/ml-network/ip/127.0.0.1?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Single IP with a value for the Query: +export const mlNetworkSingleIpKqlQuery = + "/app/security#/ml-network/ip/127.0.0.1?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Multiple IPs with a null for the Query: +export const mlNetworkMultipleIpNullKqlQuery = + "/app/security#/ml-network/ip/127.0.0.1,127.0.0.2?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Multiple IPs with a value for the Query: +export const mlNetworkMultipleIpKqlQuery = + "/app/security#/ml-network/ip/127.0.0.1,127.0.0.2?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// $ip$ with a null Query: +export const mlNetworkNullKqlQuery = + "/app/security#/ml-network/ip/$ip$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// $ip$ with a value for the Query: +export const mlNetworkKqlQuery = + "/app/security#/ml-network/ip/$ip$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + +// Single host name with a null for the Query: +export const mlHostSingleHostNullKqlQuery = + "/app/security#/ml-hosts/siem-windows?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Single host name with a variable in the Query: +export const mlHostSingleHostKqlQueryVariable = + "/app/security#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22$process.name$%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Single host name with a value for Query: +export const mlHostSingleHostKqlQuery = + "/app/security#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Multiple host names with null for Query: +export const mlHostMultiHostNullKqlQuery = + "/app/security#/ml-hosts/siem-windows,siem-suricata?query=!n&&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Multiple host names with a value for Query: +export const mlHostMultiHostKqlQuery = + "/app/security#/ml-hosts/siem-windows,siem-suricata?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Undefined/null host name with a null for the KQL: +export const mlHostVariableHostNullKqlQuery = + "/app/security#/ml-hosts/$host.name$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + +// Undefined/null host name but with a value for Query: +export const mlHostVariableHostKqlQuery = + "/app/security#/ml-hosts/$host.name$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts new file mode 100644 index 0000000000000..9bfe2e9e5102e --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -0,0 +1,19 @@ +/* + * 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 CASES = '/app/security#/case'; +export const DETECTIONS = 'app/security#/detections'; +export const HOSTS_PAGE = '/app/security#/hosts/allHosts'; +export const HOSTS_PAGE_TAB_URLS = { + allHosts: '/app/security#/hosts/allHosts', + anomalies: '/app/security#/hosts/anomalies', + authentications: '/app/security#/hosts/authentications', + events: '/app/security#/hosts/events', + uncommonProcesses: '/app/security#/hosts/uncommonProcesses', +}; +export const NETWORK_PAGE = '/app/security#/network'; +export const OVERVIEW_PAGE = '/app/security#/overview'; +export const TIMELINES_PAGE = '/app/security#/timelines'; diff --git a/x-pack/plugins/security_solution/cypress/urls/state.ts b/x-pack/plugins/security_solution/cypress/urls/state.ts new file mode 100644 index 0000000000000..6de30fdafdaf8 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/urls/state.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. + */ + +export const ABSOLUTE_DATE_RANGE = { + url: + '/app/security#/network/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + + urlUnlinked: + '/app/security#/network/?timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', + urlKqlNetworkNetwork: `/app/security#/network/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlNetworkHosts: `/app/security#/network/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsNetwork: `/app/security#/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsHosts: `/app/security#/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlHost: + '/app/security#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + urlHostNew: + '/app/security#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))', +}; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json new file mode 100644 index 0000000000000..8ce8820a8e57d --- /dev/null +++ b/x-pack/plugins/security_solution/kibana.json @@ -0,0 +1,32 @@ +{ + "id": "securitySolution", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "securitySolution"], + "requiredPlugins": [ + "actions", + "alerts", + "data", + "dataEnhanced", + "embeddable", + "features", + "home", + "ingestManager", + "inspector", + "licensing", + "maps", + "triggers_actions_ui", + "uiActions" + ], + "optionalPlugins": [ + "encryptedSavedObjects", + "ml", + "newsfeed", + "security", + "spaces", + "usageCollection", + "lists" + ], + "server": true, + "ui": true +} diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json new file mode 100644 index 0000000000000..73347e00e6b34 --- /dev/null +++ b/x-pack/plugins/security_solution/package.json @@ -0,0 +1,24 @@ +{ + "author": "Elastic", + "name": "security_solution", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "build-graphql-types": "node scripts/generate_types_from_graphql.js", + "cypress:open": "cypress open --config-file ./cypress/cypress.json", + "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-security-solution/cypress/results > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", + "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.ts", + "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" + }, + "devDependencies": { + "@types/lodash": "^4.14.110" + }, + "dependencies": { + "lodash": "^4.17.15", + "querystring": "^0.2.0", + "redux-devtools-extension": "^2.13.8", + "@types/seedrandom": ">=2.0.0 <4.0.0" + } +} diff --git a/x-pack/plugins/siem/public/alerts/components/activity_monitor/columns.tsx b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/columns.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/activity_monitor/columns.tsx rename to x-pack/plugins/security_solution/public/alerts/components/activity_monitor/columns.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/activity_monitor/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/activity_monitor/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/activity_monitor/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/activity_monitor/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/activity_monitor/types.ts b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/activity_monitor/types.ts rename to x-pack/plugins/security_solution/public/alerts/components/activity_monitor/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/config.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/config.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/config.ts rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/config.ts diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts new file mode 100644 index 0000000000000..6eaa0ba3fc4ec --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STACK_BY_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel', + { + defaultMessage: 'Stack by', + } +); + +export const STACK_BY_RISK_SCORES = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown', + { + defaultMessage: 'Risk scores', + } +); + +export const STACK_BY_SEVERITIES = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown', + { + defaultMessage: 'Severities', + } +); + +export const STACK_BY_DESTINATION_IPS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown', + { + defaultMessage: 'Top destination IPs', + } +); + +export const STACK_BY_SOURCE_IPS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown', + { + defaultMessage: 'Top source IPs', + } +); + +export const STACK_BY_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown', + { + defaultMessage: 'Top event actions', + } +); + +export const STACK_BY_CATEGORIES = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown', + { + defaultMessage: 'Top event categories', + } +); + +export const STACK_BY_HOST_NAMES = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown', + { + defaultMessage: 'Top host names', + } +); + +export const STACK_BY_RULE_TYPES = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown', + { + defaultMessage: 'Top rule types', + } +); + +export const STACK_BY_RULE_NAMES = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown', + { + defaultMessage: 'Top rules', + } +); + +export const STACK_BY_USERS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.usersDropDown', + { + defaultMessage: 'Top users', + } +); + +export const TOP = (fieldName: string) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel', { + values: { fieldName }, + defaultMessage: `Top {fieldName}`, + }); + +export const HISTOGRAM_HEADER = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle', + { + defaultMessage: 'Alert count', + } +); + +export const ALL_OTHERS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); + +export const VIEW_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel', + { + defaultMessage: 'View alerts', + } +); + +export const SHOWING_ALERTS = ( + totalAlertsFormatted: string, + totalAlerts: number, + modifier: string +) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts, modifier }, + defaultMessage: + 'Showing: {modifier}{totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', + }); diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/types.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/types.ts rename to x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_info/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_info/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_info/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_info/query.dsl.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_info/query.dsl.ts rename to x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_info/types.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_info/types.ts rename to x-pack/plugins/security_solution/public/alerts/components/alerts_info/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/actions.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/actions.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts new file mode 100644 index 0000000000000..9427464e23726 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SHOWING_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.utilityBar.showingAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts }, + defaultMessage: + 'Showing {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', + }); + +export const SELECTED_ALERTS = (selectedAlertsFormatted: string, selectedAlerts: number) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.utilityBar.selectedAlertsTitle', { + values: { selectedAlertsFormatted, selectedAlerts }, + defaultMessage: + 'Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {alert} other {alerts}}', + }); + +export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.utilityBar.selectAllAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts }, + defaultMessage: + 'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', + }); + +export const CLEAR_SELECTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle', + { + defaultMessage: 'Clear selection', + } +); + +export const BATCH_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActionsTitle', + { + defaultMessage: 'Batch actions', + } +); + +export const BATCH_ACTION_VIEW_SELECTED_IN_HOSTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.viewSelectedInHostsTitle', + { + defaultMessage: 'View selected in hosts', + } +); + +export const BATCH_ACTION_VIEW_SELECTED_IN_NETWORK = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.viewSelectedInNetworkTitle', + { + defaultMessage: 'View selected in network', + } +); + +export const BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.viewSelectedInTimelineTitle', + { + defaultMessage: 'View selected in timeline', + } +); + +export const BATCH_ACTION_OPEN_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.openSelectedTitle', + { + defaultMessage: 'Open selected', + } +); + +export const BATCH_ACTION_CLOSE_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle', + { + defaultMessage: 'Close selected', + } +); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.test.tsx new file mode 100644 index 0000000000000..1b9070ff83ac7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.test.tsx @@ -0,0 +1,195 @@ +/* + * 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 { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { buildAlertsRuleIdFilter } from './default_config'; + +jest.mock('./actions'); + +describe('alerts default_config', () => { + describe('buildAlertsRuleIdFilter', () => { + test('given a rule id this will return an array with a single filter', () => { + const filters: Filter[] = buildAlertsRuleIdFilter('rule-id-1'); + const expectedFilter: Filter = { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.rule.id', + params: { + query: 'rule-id-1', + }, + }, + query: { + match_phrase: { + 'signal.rule.id': 'rule-id-1', + }, + }, + }; + expect(filters).toHaveLength(1); + expect(filters[0]).toEqual(expectedFilter); + }); + }); + // TODO: move these tests to ../timelines/components/timeline/body/events/event_column_view.tsx + // describe.skip('getAlertActions', () => { + // let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; + // let setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; + // let createTimeline: CreateTimeline; + // let updateTimelineIsLoading: UpdateTimelineLoading; + // + // let onAlertStatusUpdateSuccess: (count: number, status: string) => void; + // let onAlertStatusUpdateFailure: (status: string, error: Error) => void; + // + // beforeEach(() => { + // setEventsLoading = jest.fn(); + // setEventsDeleted = jest.fn(); + // createTimeline = jest.fn(); + // updateTimelineIsLoading = jest.fn(); + // onAlertStatusUpdateSuccess = jest.fn(); + // onAlertStatusUpdateFailure = jest.fn(); + // }); + // + // describe('timeline tooltip', () => { + // test('it invokes sendAlertToTimelineAction when button clicked', () => { + // const alertsActions = getAlertActions({ + // canUserCRUD: true, + // hasIndexWrite: true, + // setEventsLoading, + // setEventsDeleted, + // createTimeline, + // status: 'open', + // updateTimelineIsLoading, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // const timelineAction = alertsActions[0].getAction({ + // eventId: 'even-id', + // ecsData: mockEcsDataWithAlert, + // }); + // const wrapper = mount(timelineAction as React.ReactElement); + // wrapper.find(EuiButtonIcon).simulate('click'); + // + // expect(sendAlertToTimelineAction).toHaveBeenCalled(); + // }); + // }); + // + // describe('alert open action', () => { + // let alertsActions: TimelineAction[]; + // let alertOpenAction: JSX.Element; + // let wrapper: ReactWrapper; + // + // beforeEach(() => { + // alertsActions = getAlertActions({ + // canUserCRUD: true, + // hasIndexWrite: true, + // setEventsLoading, + // setEventsDeleted, + // createTimeline, + // status: 'open', + // updateTimelineIsLoading, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // + // alertOpenAction = alertsActions[1].getAction({ + // eventId: 'event-id', + // ecsData: mockEcsDataWithAlert, + // }); + // + // wrapper = mount(alertOpenAction as React.ReactElement); + // }); + // + // afterEach(() => { + // wrapper.unmount(); + // }); + // + // test('it invokes updateAlertStatusAction when button clicked', () => { + // wrapper.find(EuiButtonIcon).simulate('click'); + // + // expect(updateAlertStatusAction).toHaveBeenCalledWith({ + // alertIds: ['event-id'], + // status: 'open', + // setEventsLoading, + // setEventsDeleted, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // }); + // + // test('it displays expected text on hover', () => { + // const openAlert = wrapper.find(EuiToolTip); + // openAlert.simulate('mouseOver'); + // const tooltip = wrapper.find('.euiToolTipPopover').text(); + // + // expect(tooltip).toEqual(i18n.ACTION_OPEN_ALERT); + // }); + // + // test('it displays expected icon', () => { + // const icon = wrapper.find(EuiButtonIcon).props().iconType; + // + // expect(icon).toEqual('securityAlertDetected'); + // }); + // }); + // + // describe('alert close action', () => { + // let alertsActions: TimelineAction[]; + // let alertCloseAction: JSX.Element; + // let wrapper: ReactWrapper; + // + // beforeEach(() => { + // alertsActions = getAlertActions({ + // canUserCRUD: true, + // hasIndexWrite: true, + // setEventsLoading, + // setEventsDeleted, + // createTimeline, + // status: 'closed', + // updateTimelineIsLoading, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // + // alertCloseAction = alertsActions[1].getAction({ + // eventId: 'event-id', + // ecsData: mockEcsDataWithAlert, + // }); + // + // wrapper = mount(alertCloseAction as React.ReactElement); + // }); + // + // afterEach(() => { + // wrapper.unmount(); + // }); + // + // test('it invokes updateAlertStatusAction when status button clicked', () => { + // wrapper.find(EuiButtonIcon).simulate('click'); + // + // expect(updateAlertStatusAction).toHaveBeenCalledWith({ + // alertIds: ['event-id'], + // status: 'closed', + // setEventsLoading, + // setEventsDeleted, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // }); + // + // test('it displays expected text on hover', () => { + // const closeAlert = wrapper.find(EuiToolTip); + // closeAlert.simulate('mouseOver'); + // const tooltip = wrapper.find('.euiToolTipPopover').text(); + // expect(tooltip).toEqual(i18n.ACTION_CLOSE_ALERT); + // }); + // + // test('it displays expected icon', () => { + // const icon = wrapper.find(EuiButtonIcon).props().iconType; + // + // expect(icon).toEqual('securityAlertResolved'); + // }); + // }); + // }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx similarity index 75% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx index 6cef2e7c53c46..201c46068458b 100644 --- a/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx @@ -6,14 +6,12 @@ /* eslint-disable react/display-name */ -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import ApolloClient from 'apollo-client'; -import React from 'react'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; import { - TimelineAction, - TimelineActionProps, + TimelineRowAction, + TimelineRowActionOnClick, } from '../../../timelines/components/timeline/body/actions'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { @@ -97,7 +95,7 @@ export const alertsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, id: '@timestamp', - width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + width: DEFAULT_DATE_COLUMN_MIN_WIDTH + 5, }, { columnHeaderType: defaultColumnHeaderType, @@ -110,7 +108,7 @@ export const alertsHeaders: ColumnHeaderOptions[] = [ columnHeaderType: defaultColumnHeaderType, id: 'signal.rule.version', label: i18n.ALERTS_HEADERS_VERSION, - width: 100, + width: 95, }, { columnHeaderType: defaultColumnHeaderType, @@ -192,75 +190,59 @@ export const requiredFieldsForActions = [ export const getAlertActions = ({ apolloClient, canUserCRUD, + createTimeline, hasIndexWrite, - setEventsLoading, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, setEventsDeleted, - createTimeline, + setEventsLoading, status, updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, }: { apolloClient?: ApolloClient<{}>; canUserCRUD: boolean; + createTimeline: CreateTimeline; hasIndexWrite: boolean; - setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; + onAlertStatusUpdateFailure: (status: string, error: Error) => void; + onAlertStatusUpdateSuccess: (count: number, status: string) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - createTimeline: CreateTimeline; + setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; status: 'open' | 'closed'; updateTimelineIsLoading: UpdateTimelineLoading; - onAlertStatusUpdateSuccess: (count: number, status: string) => void; - onAlertStatusUpdateFailure: (status: string, error: Error) => void; -}): TimelineAction[] => [ +}): TimelineRowAction[] => [ { - getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( - - - sendAlertToTimelineAction({ - apolloClient, - createTimeline, - ecsData, - updateTimelineIsLoading, - }) - } - iconType="timeline" - aria-label="Next" - /> - - ), + ariaLabel: 'Send alert to timeline', + content: i18n.ACTION_INVESTIGATE_IN_TIMELINE, + dataTestSubj: 'send-alert-to-timeline', + displayType: 'icon', + iconType: 'timeline', id: 'sendAlertToTimeline', + onClick: ({ ecsData }: TimelineRowActionOnClick) => + sendAlertToTimelineAction({ + apolloClient, + createTimeline, + ecsData, + updateTimelineIsLoading, + }), width: 26, }, { - getAction: ({ eventId }: TimelineActionProps): JSX.Element => ( - - - updateAlertStatusAction({ - alertIds: [eventId], - status, - setEventsLoading, - setEventsDeleted, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }) - } - isDisabled={!canUserCRUD || !hasIndexWrite} - iconType={status === FILTER_OPEN ? 'securityAlertDetected' : 'securityAlertResolved'} - aria-label="Next" - /> - - ), + ariaLabel: 'Update alert status', + content: status === FILTER_OPEN ? i18n.ACTION_OPEN_ALERT : i18n.ACTION_CLOSE_ALERT, + dataTestSubj: 'update-alert-status', + displayType: 'icon', + iconType: status === FILTER_OPEN ? 'securitySignalDetected' : 'securitySignalResolved', id: 'updateAlertStatus', + isActionDisabled: !canUserCRUD || !hasIndexWrite, + onClick: ({ eventId }: TimelineRowActionOnClick) => + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + status, + }), width: 26, }, ]; diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.test.ts rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.ts rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx new file mode 100644 index 0000000000000..685e66e73ced2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx @@ -0,0 +1,408 @@ +/* + * 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 { EuiPanel, EuiLoadingContent } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; +import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { StatefulEventsViewer } from '../../../common/components/events_viewer'; +import { HeaderSection } from '../../../common/components/header_section'; +import { combineQueries } from '../../../timelines/components/timeline/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { inputsSelectors, State, inputsModel } from '../../../common/store'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { useApolloClient } from '../../../common/utils/apollo_context'; + +import { updateAlertStatusAction } from './actions'; +import { + getAlertActions, + requiredFieldsForActions, + alertsClosedFilters, + alertsDefaultModel, + alertsOpenFilters, +} from './default_config'; +import { + FILTER_CLOSED, + FILTER_OPEN, + AlertFilterOption, + AlertsTableFilterGroup, +} from './alerts_filter_group'; +import { AlertsUtilityBar } from './alerts_utility_bar'; +import * as i18n from './translations'; +import { + CreateTimelineProps, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateAlertsStatusCallback, + UpdateAlertsStatusProps, +} from './types'; +import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; +import { + useStateToaster, + displaySuccessToast, + displayErrorToast, +} from '../../../common/components/toasters'; + +export const ALERTS_TABLE_TIMELINE_ID = 'alerts-table'; + +interface OwnProps { + canUserCRUD: boolean; + defaultFilters?: Filter[]; + hasIndexWrite: boolean; + from: number; + loading: boolean; + signalsIndex: string; + to: number; +} + +type AlertsTableComponentProps = OwnProps & PropsFromRedux; + +export const AlertsTableComponent: React.FC = ({ + canUserCRUD, + clearEventsDeleted, + clearEventsLoading, + clearSelected, + defaultFilters, + from, + globalFilters, + globalQuery, + hasIndexWrite, + isSelectAllChecked, + loading, + loadingEventIds, + selectedEventIds, + setEventsDeleted, + setEventsLoading, + signalsIndex, + to, + updateTimeline, + updateTimelineIsLoading, +}) => { + const [selectAll, setSelectAll] = useState(false); + const apolloClient = useApolloClient(); + + const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + signalsIndex !== '' ? [signalsIndex] : [] + ); + const kibana = useKibana(); + const [, dispatchToaster] = useStateToaster(); + + const getGlobalQuery = useCallback(() => { + if (browserFields != null && indexPatterns != null) { + return combineQueries({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders: [], + indexPattern: indexPatterns, + browserFields, + filters: isEmpty(defaultFilters) + ? globalFilters + : [...(defaultFilters ?? []), ...globalFilters], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + start: from, + end: to, + isEventViewer: true, + }); + } + return null; + }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); + + // Callback for creating a new timeline -- utilized by row/batch actions + const createTimelineCallback = useCallback( + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimeline({ + duplicate: true, + from: fromTimeline, + id: 'timeline-1', + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [updateTimeline, updateTimelineIsLoading] + ); + + const setEventsLoadingCallback = useCallback( + ({ eventIds, isLoading }: SetEventsLoadingProps) => { + setEventsLoading!({ id: ALERTS_TABLE_TIMELINE_ID, eventIds, isLoading }); + }, + [setEventsLoading, ALERTS_TABLE_TIMELINE_ID] + ); + + const setEventsDeletedCallback = useCallback( + ({ eventIds, isDeleted }: SetEventsDeletedProps) => { + setEventsDeleted!({ id: ALERTS_TABLE_TIMELINE_ID, eventIds, isDeleted }); + }, + [setEventsDeleted, ALERTS_TABLE_TIMELINE_ID] + ); + + const onAlertStatusUpdateSuccess = useCallback( + (count: number, status: string) => { + const title = + status === 'closed' + ? i18n.CLOSED_ALERT_SUCCESS_TOAST(count) + : i18n.OPENED_ALERT_SUCCESS_TOAST(count); + + displaySuccessToast(title, dispatchToaster); + }, + [dispatchToaster] + ); + + const onAlertStatusUpdateFailure = useCallback( + (status: string, error: Error) => { + const title = + status === 'closed' ? i18n.CLOSED_ALERT_FAILED_TOAST : i18n.OPENED_ALERT_FAILED_TOAST; + displayErrorToast(title, [error.message], dispatchToaster); + }, + [dispatchToaster] + ); + + // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar + useEffect(() => { + if (!isSelectAllChecked) { + setShowClearSelectionAction(false); + } else { + setSelectAll(false); + } + }, [isSelectAllChecked]); + + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: AlertFilterOption) => { + clearEventsLoading!({ id: ALERTS_TABLE_TIMELINE_ID }); + clearEventsDeleted!({ id: ALERTS_TABLE_TIMELINE_ID }); + clearSelected!({ id: ALERTS_TABLE_TIMELINE_ID }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + + // Callback for clearing entire selection from utility bar + const clearSelectionCallback = useCallback(() => { + clearSelected!({ id: ALERTS_TABLE_TIMELINE_ID }); + setSelectAll(false); + setShowClearSelectionAction(false); + }, [clearSelected, setSelectAll, setShowClearSelectionAction]); + + // Callback for selecting all events on all pages from utility bar + // Dispatches to stateful_body's selectAll via TimelineTypeContext props + // as scope of response data required to actually set selectedEvents + const selectAllCallback = useCallback(() => { + setSelectAll(true); + setShowClearSelectionAction(true); + }, [setSelectAll, setShowClearSelectionAction]); + + const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback( + async (refetchQuery: inputsModel.Refetch, { alertIds, status }: UpdateAlertsStatusProps) => { + await updateAlertStatusAction({ + query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, + alertIds: Object.keys(selectedEventIds), + status, + setEventsDeleted: setEventsDeletedCallback, + setEventsLoading: setEventsLoadingCallback, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, + }); + refetchQuery(); + }, + [ + getGlobalQuery, + selectedEventIds, + setEventsDeletedCallback, + setEventsLoadingCallback, + showClearSelectionAction, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, + ] + ); + + // Callback for creating the AlertsUtilityBar which receives totalCount from EventsViewer component + const utilityBarCallback = useCallback( + (refetchQuery: inputsModel.Refetch, totalCount: number) => { + return ( + 0} + clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} + isFilteredToOpen={filterGroup === FILTER_OPEN} + selectAll={selectAllCallback} + selectedEventIds={selectedEventIds} + showClearSelection={showClearSelectionAction} + totalCount={totalCount} + updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} + /> + ); + }, + [ + canUserCRUD, + hasIndexWrite, + clearSelectionCallback, + filterGroup, + loadingEventIds.length, + selectAllCallback, + selectedEventIds, + showClearSelectionAction, + updateAlertsStatusCallback, + ] + ); + + // Send to Timeline / Update Alert Status Actions for each table row + const additionalActions = useMemo( + () => + getAlertActions({ + apolloClient, + canUserCRUD, + hasIndexWrite, + createTimeline: createTimelineCallback, + setEventsLoading: setEventsLoadingCallback, + setEventsDeleted: setEventsDeletedCallback, + status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, + updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, + }), + [ + apolloClient, + canUserCRUD, + createTimelineCallback, + hasIndexWrite, + filterGroup, + setEventsLoadingCallback, + setEventsDeletedCallback, + updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, + ] + ); + const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); + const defaultFiltersMemo = useMemo(() => { + if (isEmpty(defaultFilters)) { + return filterGroup === FILTER_OPEN ? alertsOpenFilters : alertsClosedFilters; + } else if (defaultFilters != null && !isEmpty(defaultFilters)) { + return [ + ...defaultFilters, + ...(filterGroup === FILTER_OPEN ? alertsOpenFilters : alertsClosedFilters), + ]; + } + }, [defaultFilters, filterGroup]); + const { initializeTimeline, setTimelineRowActions } = useManageTimeline(); + + useEffect(() => { + initializeTimeline({ + id: ALERTS_TABLE_TIMELINE_ID, + documentType: i18n.ALERTS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_ALERTS, + loadingText: i18n.LOADING_ALERTS, + title: i18n.ALERTS_TABLE_TITLE, + selectAll: canUserCRUD ? selectAll : false, + }); + }, []); + useEffect(() => { + setTimelineRowActions({ + id: ALERTS_TABLE_TIMELINE_ID, + queryFields: requiredFieldsForActions, + timelineRowActions: additionalActions, + }); + }, [additionalActions]); + const headerFilterGroup = useMemo( + () => , + [onFilterGroupChangedCallback] + ); + + if (loading || isEmpty(signalsIndex)) { + return ( + + + + + ); + } + + return ( + + ); +}; + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getGlobalInputs = inputsSelectors.globalSelector(); + const mapStateToProps = (state: State) => { + const timeline: TimelineModel = + getTimeline(state, ALERTS_TABLE_TIMELINE_ID) ?? timelineDefaults; + const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; + + const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + return { + globalQuery: query, + globalFilters: filters, + deletedEventIds, + isSelectAllChecked, + loadingEventIds, + selectedEventIds, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + setEventsLoading: ({ + id, + eventIds, + isLoading, + }: { + id: string; + eventIds: string[]; + isLoading: boolean; + }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + setEventsDeleted: ({ + id, + eventIds, + isDeleted, + }: { + id: string; + eventIds: string[]; + isDeleted: boolean; + }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(timelineActions.updateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const AlertsTable = connector(React.memo(AlertsTableComponent)); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts new file mode 100644 index 0000000000000..07cc28681387d --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.securitySolution.detectionEngine.pageTitle', { + defaultMessage: 'Detection engine', +}); + +export const ALERTS_TABLE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.tableTitle', + { + defaultMessage: 'Alert list', + } +); + +export const ALERTS_DOCUMENT_TYPE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.documentTypeTitle', + { + defaultMessage: 'Alerts', + } +); + +export const OPEN_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.openAlertsTitle', + { + defaultMessage: 'Open alerts', + } +); + +export const CLOSED_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.closedAlertsTitle', + { + defaultMessage: 'Closed alerts', + } +); + +export const LOADING_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.loadingAlertsTitle', + { + defaultMessage: 'Loading Alerts', + } +); + +export const TOTAL_COUNT_OF_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.totalCountOfAlertsTitle', + { + defaultMessage: 'alerts match the search criteria', + } +); + +export const ALERTS_HEADERS_RULE = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const ALERTS_HEADERS_VERSION = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.versionTitle', + { + defaultMessage: 'Version', + } +); + +export const ALERTS_HEADERS_METHOD = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.methodTitle', + { + defaultMessage: 'Method', + } +); + +export const ALERTS_HEADERS_SEVERITY = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const ALERTS_HEADERS_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.riskScoreTitle', + { + defaultMessage: 'Risk Score', + } +); + +export const ACTION_OPEN_ALERT = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.openAlertTitle', + { + defaultMessage: 'Open alert', + } +); + +export const ACTION_CLOSE_ALERT = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.closeAlertTitle', + { + defaultMessage: 'Close alert', + } +); + +export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle', + { + defaultMessage: 'Investigate in timeline', + } +); + +export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.closedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.openedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const CLOSED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.closedAlertFailedToastMessage', + { + defaultMessage: 'Failed to close alert(s).', + } +); + +export const OPENED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.openedAlertFailedToastMessage', + { + defaultMessage: 'Failed to open alert(s)', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/alerts_table/types.ts rename to x-pack/plugins/security_solution/public/alerts/components/alerts_table/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/translations.ts new file mode 100644 index 0000000000000..f59be16923805 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_BADGE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel', + { + defaultMessage: 'Beta', + } +); + +export const PAGE_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip', + { + defaultMessage: + 'Alerts is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/translations.ts new file mode 100644 index 0000000000000..bc08a13c8b5d1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_API_INTEGRATION_KEY_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.noApiIntegrationKeyCallOutTitle', + { + defaultMessage: 'API integration key required', + } +); + +export const NO_API_INTEGRATION_KEY_CALLOUT_MSG = i18n.translate( + 'xpack.securitySolution.detectionEngine.noApiIntegrationKeyCallOutMsg', + { + defaultMessage: `A new encryption key is generated for saved objects each time you start Kibana. Without a persistent key, you cannot delete or modify rules after Kibana restarts. To set a persistent key, add the xpack.encryptedSavedObjects.encryptionKey setting with any text value of 32 or more characters to the kibana.yml file.`, + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.securitySolution.detectionEngine.dismissNoApiIntegrationKeyButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts new file mode 100644 index 0000000000000..d036c422b2fb9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_WRITE_ALERTS_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.noWriteAlertsCallOutTitle', + { + defaultMessage: 'Alerts index permissions required', + } +); + +export const NO_WRITE_ALERTS_CALLOUT_MSG = i18n.translate( + 'xpack.securitySolution.detectionEngine.noWriteAlertsCallOutMsg', + { + defaultMessage: + 'You are currently missing the required permissions to update alerts. Please contact your administrator for further assistance.', + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.securitySolution.detectionEngine.dismissNoWriteAlertButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/actions_description.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/actions_description.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/assets/list_tree_icon.svg b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/assets/list_tree_icon.svg similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/assets/list_tree_icon.svg rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/assets/list_tree_icon.svg diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx new file mode 100644 index 0000000000000..b82d1c0a36ab2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx @@ -0,0 +1,407 @@ +/* + * 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 { shallow } from 'enzyme'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { + esFilters, + FilterManager, + UI_SETTINGS, +} from '../../../../../../../../src/plugins/data/public'; +import { SeverityBadge } from '../severity_badge'; + +import * as i18n from './translations'; +import { + isNotEmptyArray, + buildQueryBarDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildStringArrayDescription, + buildSeverityDescription, + buildUrlsDescription, + buildNoteDescription, + buildRuleTypeDescription, +} from './helpers'; +import { ListItems } from './types'; + +const setupMock = coreMock.createSetup(); +const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } +}; +setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); +const mockFilterManager = new FilterManager(setupMock.uiSettings); + +const mockQueryBar = { + query: 'test query', + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +describe('helpers', () => { + describe('isNotEmptyArray', () => { + test('returns false if empty array', () => { + const result = isNotEmptyArray([]); + expect(result).toBeFalsy(); + }); + + test('returns false if array of empty strings', () => { + const result = isNotEmptyArray(['', '']); + expect(result).toBeFalsy(); + }); + + test('returns true if array of string with space', () => { + const result = isNotEmptyArray([' ']); + expect(result).toBeTruthy(); + }); + + test('returns true if array with at least one non-empty string', () => { + const result = isNotEmptyArray(['', 'abc']); + expect(result).toBeTruthy(); + }); + }); + + describe('buildQueryBarDescription', () => { + test('returns empty array if no filters, query or savedId exist', () => { + const emptyMockQueryBar = { + query: '', + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: emptyMockQueryBar.filters, + filterManager: mockFilterManager, + query: emptyMockQueryBar.query, + savedId: emptyMockQueryBar.saved_id, + }); + expect(result).toEqual([]); + }); + + test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + }); + + test('returns expected array of ListItems when filters AND indexPatterns exist', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); + expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); + }); + + test('returns expected array of ListItems when "query.query" exists', () => { + const mockQueryBarWithQuery = { + ...mockQueryBar, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithQuery.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithQuery.query, + savedId: mockQueryBarWithQuery.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} ); + }); + + test('returns expected array of ListItems when "savedId" exists', () => { + const mockQueryBarWithSavedId = { + ...mockQueryBar, + query: '', + filters: [], + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithSavedId.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithSavedId.query, + savedId: mockQueryBarWithSavedId.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} ); + }); + }); + + describe('buildThreatDescription', () => { + test('returns empty array if no threats', () => { + const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); + expect(result).toHaveLength(0); + }); + + test('returns empty tactic link if no corresponding tactic id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns empty technique link if no corresponding technique id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); + }); + + test('returns with corresponding tactic and technique link text', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns corresponding number of tactic and technique links', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, + { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, + ], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, + ], + tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); + }); + }); + + describe('buildUnorderedListArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + [] + ); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + ['', 'falsePositive1', 'falsePositive2'] + ); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); + }); + }); + + describe('buildStringArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ + '', + 'tag1', + 'tag2', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); + expect( + wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]').first().text() + ).toEqual('tag1'); + expect( + wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]').at(1).text() + ).toEqual('tag2'); + }); + }); + + describe('buildSeverityDescription', () => { + test('returns ListItem with passed in label and SeverityBadge component', () => { + const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + + expect(result[0].title).toEqual('Test label'); + expect(result[0].description).toEqual(); + }); + }); + + describe('buildUrlsDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUrlsDescription('Test label', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUrlsDescription('Test label', [ + 'www.test.com', + 'www.test2.com', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); + expect( + wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]').first().text() + ).toEqual('www.test.com'); + expect( + wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]').at(1).text() + ).toEqual('www.test2.com'); + }); + }); + + describe('buildNoteDescription', () => { + test('returns ListItem with passed in label and note content', () => { + const noteSample = + 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; + const result: ListItems[] = buildNoteDescription('Test label', noteSample); + const wrapper = shallow(result[0].description as React.ReactElement); + const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); + + expect(result[0].title).toEqual('Test label'); + expect(noteElement.exists()).toBeTruthy(); + expect(noteElement.text()).toEqual(noteSample); + }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildNoteDescription('Test label', ''); + + expect(result).toHaveLength(0); + }); + }); + + describe('buildRuleTypeDescription', () => { + it('returns the label for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.description).toEqual('Machine Learning'); + }); + + it('returns the label for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.description).toEqual('Query'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx new file mode 100644 index 0000000000000..b8f81f6d7e5f7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx @@ -0,0 +1,476 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { + StepRuleDescriptionComponent, + addFilterStateIfNotThere, + buildListItems, + getDescriptionItem, +} from '.'; + +import { + esFilters, + Filter, + FilterManager, + UI_SETTINGS, +} from '../../../../../../../../src/plugins/data/public'; +import { + mockAboutStepRule, + mockDefineStepRule, +} from '../../../pages/detection_engine/rules/all/__mocks__/mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; +import * as i18n from './translations'; + +import { schema } from '../step_about_rule/schema'; +import { ListItems } from './types'; +import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; + +jest.mock('../../../../common/lib/kibana'); + +describe('description_step', () => { + const setupMock = coreMock.createSetup(); + const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } + }; + let mockFilterManager: FilterManager; + let mockAboutStep: AboutStepRule; + + beforeEach(() => { + setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); + mockFilterManager = new FilterManager(setupMock.uiSettings); + mockAboutStep = mockAboutStepRule(); + }); + + describe('StepRuleDescriptionComponent', () => { + test('renders correctly against snapshot when columns is "multi"', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); + }); + + test('renders correctly against snapshot when columns is "single"', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + }); + + test('renders correctly against snapshot when columns is "singleSplit', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + expect( + wrapper.find('[data-test-subj="singleSplitStepRuleDescriptionList"]').at(0).prop('type') + ).toEqual('column'); + }); + }); + + describe('addFilterStateIfNotThere', () => { + test('it does not change the state if it is global', () => { + const filters: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + + test('it adds the state if it does not exist as local', () => { + const filters: Filter[] = [ + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + }); + + describe('buildListItems', () => { + test('returns expected ListItems array when given valid inputs', () => { + const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); + + expect(result.length).toEqual(9); + }); + }); + + describe('getDescriptionItem', () => { + test('returns ListItem with all values enumerated when value[field] is an array', () => { + const result: ListItems[] = getDescriptionItem( + 'tags', + 'Tags label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Tags label'); + expect(typeof result[0].description).toEqual('object'); + }); + + test('returns ListItem with description of value[field] when value[field] is a string', () => { + const result: ListItems[] = getDescriptionItem( + 'description', + 'Description label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Description label'); + expect(result[0].description).toEqual('24/7'); + }); + + test('returns empty array when "value" is a non-existant property in "field"', () => { + const result: ListItems[] = getDescriptionItem( + 'jibberjabber', + 'JibberJabber label', + mockAboutStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + + describe('queryBar', () => { + test('returns array of ListItems when queryBar exist', () => { + const mockQueryBar = { + isNew: false, + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: null, + saved_id: null, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'queryBar', + 'Query bar label', + mockQueryBar, + mockFilterManager + ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} ); + }); + }); + + describe('threat', () => { + test('returns array of ListItems when threat exist', () => { + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threat label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + + test('filters out threats with tactic.name of "none"', () => { + const mockStep = { + ...mockAboutStep, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + }); + + describe('references', () => { + test('returns array of ListItems when references exist', () => { + const result: ListItems[] = getDescriptionItem( + 'references', + 'Reference label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Reference label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('falsePositives', () => { + test('returns array of ListItems when falsePositives exist', () => { + const result: ListItems[] = getDescriptionItem( + 'falsePositives', + 'False positives label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('False positives label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('severity', () => { + test('returns array of ListItems when severity exist', () => { + const result: ListItems[] = getDescriptionItem( + 'severity', + 'Severity label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Severity label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('riskScore', () => { + test('returns array of ListItems when riskScore exist', () => { + const result: ListItems[] = getDescriptionItem( + 'riskScore', + 'Risk score label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Risk score label'); + expect(result[0].description).toEqual(21); + }); + }); + + describe('timeline', () => { + test('returns timeline title if one exists', () => { + const mockDefineStep = mockDefineStepRule(); + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockDefineStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual('Titled timeline'); + }); + + test('returns default timeline title if none exists', () => { + const mockStep = { + ...mockDefineStepRule(), + timeline: { + id: '12345', + }, + }; + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); + }); + }); + + describe('note', () => { + test('returns default "note" description', () => { + const result: ListItems[] = getDescriptionItem( + 'note', + 'Investigation guide', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Investigation guide'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/throttle_description.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/throttle_description.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/throttle_description.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/throttle_description.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/translations.tsx new file mode 100644 index 0000000000000..3e639ede7a18b --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/translations.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILTERS_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.filtersLabel', + { + defaultMessage: 'Filters', + } +); + +export const QUERY_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.QueryLabel', + { + defaultMessage: 'Custom query', + } +); + +export const SAVED_ID_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.savedIdLabel', + { + defaultMessage: 'Saved query name', + } +); + +export const ML_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.mlRuleTypeDescription', + { + defaultMessage: 'Machine Learning', + } +); + +export const QUERY_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.queryRuleTypeDescription', + { + defaultMessage: 'Query', + } +); + +export const ML_JOB_STARTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription', + { + defaultMessage: 'Started', + } +); + +export const ML_JOB_STOPPED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription', + { + defaultMessage: 'Stopped', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/types.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/description_step/types.ts rename to x-pack/plugins/security_solution/public/alerts/components/rules/description_step/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.ts rename to x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/mitre/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/mitre/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/translations.ts new file mode 100644 index 0000000000000..704f950cfb4b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/translations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const TACTIC = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.tacticsDescription', + { + defaultMessage: 'tactic', + } +); + +export const TECHNIQUE = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.techniquesDescription', + { + defaultMessage: 'techniques', + } +); + +export const ADD_MITRE_ATTACK = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.addTitle', + { + defaultMessage: 'Add MITRE ATT&CK\\u2122 threat', + } +); + +export const TECHNIQUES_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.techniquesPlaceHolderDescription', + { + defaultMessage: 'Select techniques ...', + } +); + +export const TACTIC_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription', + { + defaultMessage: 'Select tactic ...', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.tsx new file mode 100644 index 0000000000000..cb084d4daa782 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.tsx @@ -0,0 +1,140 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; + +import styled from 'styled-components'; +import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + ML_JOB_SELECT_PLACEHOLDER_TEXT, + ENABLE_ML_JOB_WARNING, +} from '../step_define_rule/translations'; + +const HelpTextWarningContainer = styled.div` + margin-top: 10px; +`; + +const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` + margin-bottom: 5px; +`; + +const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ + href, + showEnableWarning = false, +}) => ( + <> + + + + ), + }} + /> + {showEnableWarning && ( + + + + {ENABLE_ML_JOB_WARNING} + + + )} + +); + +const JobDisplay: React.FC<{ title: string; description: string }> = ({ title, description }) => ( + <> + {title} + +

{description}

+ + +); + +interface MlJobSelectProps { + describedByIds: string[]; + field: FieldHook; +} + +export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { + const jobId = field.value as string; + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [isLoading, siemJobs] = useSiemJobs(false); + const mlUrl = useKibana().services.application.getUrlForApp('ml'); + const handleJobChange = useCallback( + (machineLearningJobId: string) => { + field.setValue(machineLearningJobId); + }, + [field] + ); + const placeholderOption = { + value: 'placeholder', + inputDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + dropdownDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + disabled: true, + }; + + const jobOptions = siemJobs.map((job) => ({ + value: job.id, + inputDisplay: job.id, + dropdownDisplay: , + })); + + const options = [placeholderOption, ...jobOptions]; + + const isJobRunning = useMemo(() => { + // If the selected job is not found in the list, it means the placeholder is selected + // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' + const job = siemJobs.find((j) => j.id === jobId); + return job == null || isJobStarted(job.jobState, job.datafeedState); + }, [siemJobs, jobId]); + + return ( + + + } + isInvalid={isInvalid} + error={errorMessage} + data-test-subj="mlJobSelect" + describedByIds={describedByIds} + > + + + + + + + + + ); +}; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/alerts/components/rules/next_step/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/next_step/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/next_step/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/next_step/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts new file mode 100644 index 0000000000000..b43ef8c615003 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PRE_BUILT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle', + { + defaultMessage: 'Load Elastic prebuilt detection rules', + } +); + +export const PRE_BUILT_MSG = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage', + { + defaultMessage: + 'Elastic SIEM comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', + } +); + +export const PRE_BUILT_ACTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton', + { + defaultMessage: 'Load prebuilt detection rules', + } +); + +export const CREATE_RULE_ACTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton', + { + defaultMessage: 'Create your own rules', + } +); + +export const UPDATE_PREPACKAGED_RULES_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesTitle', + { + defaultMessage: 'Update available for Elastic prebuilt rules', + } +); + +export const UPDATE_PREPACKAGED_RULES_MSG = (updateRules: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesMsg', { + values: { updateRules }, + defaultMessage: + 'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}. Note that this will reload deleted Elastic prebuilt rules.', + }); + +export const UPDATE_PREPACKAGED_RULES = (updateRules: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesButton', { + values: { updateRules }, + defaultMessage: + 'Update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}} ', + }); + +export const RELEASE_NOTES_HELP = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.releaseNotesHelp', + { + defaultMessage: 'Release notes', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/translations.tsx new file mode 100644 index 0000000000000..756b0c05f435a --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/translations.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const IMPORT_TIMELINE_MODAL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineModalTitle', + { + defaultMessage: 'Import query from saved timeline', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/translations.ts new file mode 100644 index 0000000000000..26b6bb83a145d --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.readOnlyCallOutTitle', + { + defaultMessage: 'Rule permissions required', + } +); + +export const READ_ONLY_CALLOUT_MSG = i18n.translate( + 'xpack.securitySolution.detectionEngine.readOnlyCallOutMsg', + { + defaultMessage: + 'You are currently missing the required permissions to create/edit detection engine rule. Please contact your administrator for further assistance.', + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.securitySolution.detectionEngine.dismissButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/translations.tsx new file mode 100644 index 0000000000000..8e467307cb6da --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/translations.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const FORM_ERRORS_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.ruleActionsField.ruleActionsFormErrorsTitle', + { + defaultMessage: 'Please fix issues listed below', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/translations.ts new file mode 100644 index 0000000000000..862d78936e9d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALL_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.components.ruleActionsOverflow.allActionsTitle', + { + defaultMessage: 'All actions', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.ts rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/translations.ts new file mode 100644 index 0000000000000..30a352a83a324 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STATUS = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.statusDescription', + { + defaultMessage: 'Last response', + } +); + +export const STATUS_AT = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.statusAtDescription', + { + defaultMessage: 'at', + } +); + +export const STATUS_DATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.statusDateDescription', + { + defaultMessage: 'Status date', + } +); + +export const REFRESH = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.refreshButton', + { + defaultMessage: 'Refresh', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/translations.ts new file mode 100644 index 0000000000000..66bc6ea900925 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SECONDS = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.secondsOptionDescription', + { + defaultMessage: 'Seconds', + } +); + +export const MINUTES = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.minutesOptionDescription', + { + defaultMessage: 'Minutes', + } +); + +export const HOURS = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.hoursOptionDescription', + { + defaultMessage: 'Hours', + } +); + +export const INVALID_TIME = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.invalidTimeMessageDescription', + { + defaultMessage: 'A time is required.', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.tsx new file mode 100644 index 0000000000000..3dad53f532a5b --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.tsx @@ -0,0 +1,123 @@ +/* + * 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, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCard, + EuiFlexGrid, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiText, +} from '@elastic/eui'; + +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { RuleType } from '../../../../../common/detection_engine/types'; +import { FieldHook } from '../../../../shared_imports'; +import { useKibana } from '../../../../common/lib/kibana'; +import * as i18n from './translations'; + +const MlCardDescription = ({ + subscriptionUrl, + hasValidLicense = false, +}: { + subscriptionUrl: string; + hasValidLicense?: boolean; +}) => ( + + {hasValidLicense ? ( + i18n.ML_TYPE_DESCRIPTION + ) : ( + + +
+ ), + }} + /> + )} + +); + +interface SelectRuleTypeProps { + describedByIds?: string[]; + field: FieldHook; + hasValidLicense?: boolean; + isMlAdmin?: boolean; + isReadOnly?: boolean; +} + +export const SelectRuleType: React.FC = ({ + describedByIds = [], + field, + isReadOnly = false, + hasValidLicense = false, + isMlAdmin = false, +}) => { + const ruleType = field.value as RuleType; + const setType = useCallback( + (type: RuleType) => { + field.setValue(type); + }, + [field] + ); + const setMl = useCallback(() => setType('machine_learning'), [setType]); + const setQuery = useCallback(() => setType('query'), [setType]); + const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; + const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { + path: '#/management/stack/license_management', + }); + + return ( + + + + } + selectable={{ + isDisabled: isReadOnly, + onClick: setQuery, + isSelected: !isMlRule(ruleType), + }} + /> + + + + } + icon={} + isDisabled={mlCardDisabled} + selectable={{ + isDisabled: mlCardDisabled, + onClick: setMl, + isSelected: isMlRule(ruleType), + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/translations.ts new file mode 100644 index 0000000000000..8b92d20616f7c --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const QUERY_TYPE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeTitle', + { + defaultMessage: 'Custom query', + } +); + +export const QUERY_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeDescription', + { + defaultMessage: 'Use KQL or Lucene to detect issues across indices.', + } +); + +export const ML_TYPE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeTitle', + { + defaultMessage: 'Machine Learning', + } +); + +export const ML_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDescription', + { + defaultMessage: 'Select ML job to detect anomalous activity.', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/data.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/default_value.ts rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx new file mode 100644 index 0000000000000..42ffab10ff469 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, + ERROR_CODE, +} from '../../../../shared_imports'; +import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; +import { isMitreAttackInvalid } from '../mitre/helpers'; +import { OptionalFieldLabel } from '../optional_field_label'; +import { isUrlInvalid } from '../../../../common/utils/validators'; +import * as I18n from './translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNameLabel', + { + defaultMessage: 'Name', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError', + { + defaultMessage: 'A name is required.', + } + ) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldDescriptionLabel', + { + defaultMessage: 'Description', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } + ) + ), + }, + ], + }, + severity: { + type: FIELD_TYPES.SUPER_SELECT, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', + { + defaultMessage: 'Severity', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', + { + defaultMessage: 'A severity is required.', + } + ) + ), + }, + ], + }, + riskScore: { + type: FIELD_TYPES.RANGE, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRiskScoreLabel', + { + defaultMessage: 'Risk score', + } + ), + }, + references: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel', + { + defaultMessage: 'Reference URLs', + } + ), + labelAppend: OptionalFieldLabel, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + let hasError = false; + (value as string[]).forEach((url) => { + if (isUrlInvalid(url)) { + hasError = true; + } + }); + return hasError + ? { + code: 'ERR_FIELD_FORMAT', + path, + message: I18n.URL_FORMAT_INVALID, + } + : undefined; + }, + }, + ], + }, + falsePositives: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel', + { + defaultMessage: 'False positive examples', + } + ), + labelAppend: OptionalFieldLabel, + }, + threat: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', + { + defaultMessage: 'MITRE ATT&CK\\u2122', + } + ), + labelAppend: OptionalFieldLabel, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + let hasError = false; + (value as IMitreEnterpriseAttack[]).forEach((v) => { + if (isMitreAttackInvalid(v.tactic.name, v.technique)) { + hasError = true; + } + }); + return hasError + ? { + code: 'ERR_FIELD_MISSING', + path, + message: I18n.CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED, + } + : undefined; + }, + }, + ], + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', + { + defaultMessage: 'Tags', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText', + { + defaultMessage: + 'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.', + } + ), + labelAppend: OptionalFieldLabel, + }, + note: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel', + { + defaultMessage: 'Investigation guide', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText', + { + defaultMessage: + 'Provide helpful information for analysts that are performing a signal investigation. This guide will appear on both the rule details page and in timelines created from alerts generated by this rule.', + } + ), + labelAppend: OptionalFieldLabel, + }, +}; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/translations.ts new file mode 100644 index 0000000000000..c179128c56d92 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/translations.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADVANCED_SETTINGS = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.advancedSettingsButton', + { + defaultMessage: 'Advanced settings', + } +); + +export const ADD_REFERENCE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription', + { + defaultMessage: 'Add reference URL', + } +); + +export const ADD_FALSE_POSITIVE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription', + { + defaultMessage: 'Add false positive example', + } +); + +export const LOW = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription', + { + defaultMessage: 'Low', + } +); + +export const MEDIUM = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription', + { + defaultMessage: 'Medium', + } +); + +export const HIGH = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription', + { + defaultMessage: 'High', + } +); + +export const CRITICAL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionCriticalDescription', + { + defaultMessage: 'Critical', + } +); + +export const CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError', + { + defaultMessage: 'At least one Technique is required with a Tactic.', + } +); + +export const URL_FORMAT_INVALID = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError', + { + defaultMessage: 'Url is invalid format', + } +); + +export const ADD_RULE_NOTE_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText', + { + defaultMessage: 'Add rule investigation guide...', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/translations.ts new file mode 100644 index 0000000000000..e1b89b1ec8ce2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/translations.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const ABOUT_PANEL_DETAILS_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel', + { + defaultMessage: 'Details', + } +); + +export const ABOUT_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText', + { + defaultMessage: 'About', + } +); + +export const ABOUT_PANEL_NOTES_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.details.stepAboutRule.investigationGuideLabel', + { + defaultMessage: 'Investigation guide', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/schema.tsx new file mode 100644 index 0000000000000..190d4484b156b --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/schema.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; + +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; +import { FieldValueQueryBar } from '../query_bar'; +import { + ERROR_CODE, + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, +} from '../../../../shared_imports'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; + +export const schema: FormSchema = { + index: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', + { + defaultMessage: 'Index patterns', + } + ), + helpText: {INDEX_HELPER_TEXT}, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = !isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'A minimum of one index pattern is required.', + } + ) + )(...args); + }, + }, + ], + }, + queryBar: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', + { + defaultMessage: 'Custom query', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path, formData }] = args; + const { query, filters } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + return isEmpty(query.query as string) && isEmpty(filters) + ? { + code: 'ERR_FIELD_MISSING', + path, + message: CUSTOM_QUERY_REQUIRED, + } + : undefined; + }, + }, + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path, formData }] = args; + const { query } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + if (!isEmpty(query.query as string) && query.language === 'kuery') { + try { + esKuery.fromKueryExpression(query.query); + } catch (err) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: INVALID_CUSTOM_QUERY, + }; + } + } + }, + }, + ], + }, + ruleType: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', + { + defaultMessage: 'Rule type', + } + ), + validations: [], + }, + anomalyThreshold: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', + { + defaultMessage: 'Anomaly score threshold', + } + ), + validations: [], + }, + machineLearningJobId: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', + { + defaultMessage: 'Machine Learning job', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', + { + defaultMessage: 'A Machine Learning job is required.', + } + ) + )(...args); + }, + }, + ], + }, + timeline: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', + { + defaultMessage: 'Timeline template', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', + { + defaultMessage: 'Select which timeline to use when investigating generated alerts.', + } + ), + }, +}; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx new file mode 100644 index 0000000000000..a371db72d2ee1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CUSTOM_QUERY_REQUIRED = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError', + { + defaultMessage: 'A custom query is required.', + } +); + +export const INVALID_CUSTOM_QUERY = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError', + { + defaultMessage: 'The KQL is invalid', + } +); + +export const CONFIG_INDICES = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription', + { + defaultMessage: 'Use Elasticsearch indices from SIEM advanced settings', + } +); + +export const CUSTOM_INDICES = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesCustomDescription', + { + defaultMessage: 'Provide custom list of indices', + } +); + +export const INDEX_HELPER_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesHelperDescription', + { + defaultMessage: + 'Enter the pattern of Elasticsearch indices where you would like this rule to run. By default, these will include index patterns defined in SIEM advanced settings.', + } +); + +export const RESET_DEFAULT_INDEX = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.resetDefaultIndicesButton', + { + defaultMessage: 'Reset to default index patterns', + } +); + +export const IMPORT_TIMELINE_QUERY = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineQueryButton', + { + defaultMessage: 'Import query from saved timeline', + } +); + +export const ML_JOB_SELECT_PLACEHOLDER_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlJobSelectPlaceholderText', + { + defaultMessage: 'Select a job', + } +); + +export const ENABLE_ML_JOB_WARNING = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobWarningTitle', + { + defaultMessage: + 'This ML job is not currently running. Please set this job to run via "ML job settings" before activating this rule.', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/types.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/types.ts rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx new file mode 100644 index 0000000000000..778c6bd92bc73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx @@ -0,0 +1,228 @@ +/* + * 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 { + EuiHorizontalRule, + EuiForm, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; +import { findIndex } from 'lodash/fp'; +import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; +import { + RuleStep, + RuleStepProps, + ActionsStepRule, +} from '../../../pages/detection_engine/rules/types'; +import { StepRuleDescription } from '../description_step'; +import { Form, UseField, useForm } from '../../../../shared_imports'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { + ThrottleSelectField, + THROTTLE_OPTIONS, + DEFAULT_THROTTLE_OPTION, +} from '../throttle_select_field'; +import { RuleActionsField } from '../rule_actions_field'; +import { useKibana } from '../../../../common/lib/kibana'; +import { getSchema } from './schema'; +import * as I18n from './translations'; + +interface StepRuleActionsProps extends RuleStepProps { + defaultValues?: ActionsStepRule | null; + actionMessageParams: string[]; +} + +const stepActionsDefaultValue = { + enabled: true, + isNew: true, + actions: [], + kibanaSiemAppUrl: '', + throttle: DEFAULT_THROTTLE_OPTION.value, +}; + +const GhostFormField = () => <>; + +const getThrottleOptions = (throttle?: string | null) => { + // Add support for throttle options set by the API + if (throttle && findIndex(['value', throttle], THROTTLE_OPTIONS) < 0) { + return [...THROTTLE_OPTIONS, { value: throttle, text: throttle }]; + } + + return THROTTLE_OPTIONS; +}; + +const StepRuleActionsComponent: FC = ({ + addPadding = false, + defaultValues, + isReadOnlyView, + isLoading, + isUpdateView = false, + setStepData, + setForm, + actionMessageParams, +}) => { + const [myStepData, setMyStepData] = useState(stepActionsDefaultValue); + const { + services: { + application, + triggers_actions_ui: { actionTypeRegistry }, + }, + } = useKibana(); + const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + + const kibanaAbsoluteUrl = useMemo(() => application.getUrlForApp('siem', { absolute: true }), [ + application, + ]); + + const onSubmit = useCallback( + async (enabled: boolean) => { + if (setStepData) { + setStepData(RuleStep.ruleActions, null, false); + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.ruleActions, { ...data, enabled }, newIsValid); + setMyStepData({ ...data, isNew: false } as ActionsStepRule); + } + } + }, + [form] + ); + + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + setFieldValue(form, schema, myDefaultValues); + } + }, [defaultValues]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.ruleActions, form); + } + }, [form]); + + const updateThrottle = useCallback((throttle) => setMyStepData({ ...myStepData, throttle }), [ + myStepData, + setMyStepData, + ]); + + const throttleOptions = useMemo(() => { + const throttle = myStepData.throttle; + + return getThrottleOptions(throttle); + }, [myStepData]); + + const throttleFieldComponentProps = useMemo( + () => ({ + idAria: 'detectionEngineStepRuleActionsThrottle', + isDisabled: isLoading, + dataTestSubj: 'detectionEngineStepRuleActionsThrottle', + hasNoInitialSelection: false, + handleChange: updateThrottle, + euiFieldProps: { + options: throttleOptions, + }, + }), + [isLoading, updateThrottle] + ); + + return isReadOnlyView && myStepData != null ? ( + + + + ) : ( + <> + +
+ + + {myStepData.throttle !== stepActionsDefaultValue.throttle ? ( + <> + + + + ) : ( + + )} + + + +
+
+ + {!isUpdateView && ( + <> + + + + + {I18n.COMPLETE_WITHOUT_ACTIVATING} + + + + + {I18n.COMPLETE_WITH_ACTIVATING} + + + + + )} + + ); +}; + +export const StepRuleActions = memo(StepRuleActionsComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/schema.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/schema.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.tsx new file mode 100644 index 0000000000000..a093f991afaf7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.tsx @@ -0,0 +1,89 @@ +/* + * 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. + */ + +/* istanbul ignore file */ + +import { i18n } from '@kbn/i18n'; + +import { + AlertAction, + ActionTypeRegistryContract, +} from '../../../../../../triggers_actions_ui/public'; +import { FormSchema, FormData, ValidationFunc, ERROR_CODE } from '../../../../shared_imports'; +import * as I18n from './translations'; +import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; + +export const validateSingleAction = ( + actionItem: AlertAction, + actionTypeRegistry: ActionTypeRegistryContract +): string[] => { + if (!isUuidv4(actionItem.id)) { + return [I18n.NO_CONNECTOR_SELECTED]; + } + + const actionParamsErrors = validateActionParams(actionItem, actionTypeRegistry); + const mustacheErrors = validateMustache(actionItem.params); + + return [...actionParamsErrors, ...mustacheErrors]; +}; + +export const validateRuleActionsField = (actionTypeRegistry: ActionTypeRegistryContract) => ( + ...data: Parameters +): ReturnType> | undefined => { + const [{ value, path }] = data as [{ value: AlertAction[]; path: string }]; + + const errors = value.reduce((acc, actionItem) => { + const errorsArray = validateSingleAction(actionItem, actionTypeRegistry); + + if (errorsArray.length) { + const actionTypeName = getActionTypeName(actionItem.actionTypeId); + const errorsListItems = errorsArray.map((error) => `* ${error}\n`); + + return [...acc, `\n**${actionTypeName}:**\n${errorsListItems.join('')}`]; + } + + return acc; + }, [] as string[]); + + if (errors.length) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: `${errors.join('\n')}`, + }; + } +}; + +export const getSchema = ({ + actionTypeRegistry, +}: { + actionTypeRegistry: ActionTypeRegistryContract; +}): FormSchema => ({ + actions: { + validations: [ + { + validator: validateRuleActionsField(actionTypeRegistry), + }, + ], + }, + enabled: {}, + kibanaSiemAppUrl: {}, + throttle: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', + { + defaultMessage: 'Actions frequency', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', + { + defaultMessage: + 'Select when automated actions should be performed if a rule evaluates as true.', + } + ), + }, +}); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/translations.tsx new file mode 100644 index 0000000000000..a276cb17e95f5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/translations.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { startCase } from 'lodash/fp'; + +export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutActivatingTitle', + { + defaultMessage: 'Create rule without activating it', + } +); + +export const COMPLETE_WITH_ACTIVATING = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithActivatingTitle', + { + defaultMessage: 'Create & activate rule', + } +); + +export const NO_CONNECTOR_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noConnectorSelectedErrorMessage', + { + defaultMessage: 'No connector selected', + } +); + +export const INVALID_MUSTACHE_TEMPLATE = (paramKey: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage', + { + defaultMessage: '{key} is not valid mustache template', + values: { + key: startCase(paramKey), + }, + } + ); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/utils.test.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.test.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/utils.test.ts rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.test.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/utils.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/utils.ts rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/schema.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/schema.tsx new file mode 100644 index 0000000000000..f4c371a2364f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/schema.tsx @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/* istanbul ignore file */ + +import { i18n } from '@kbn/i18n'; + +import { OptionalFieldLabel } from '../optional_field_label'; +import { FormSchema } from '../../../../shared_imports'; + +export const schema: FormSchema = { + interval: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', + { + defaultMessage: 'Runs every', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', + { + defaultMessage: 'Rules run periodically and detect alerts within the specified time frame.', + } + ), + }, + from: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel', + { + defaultMessage: 'Additional look-back time', + } + ), + labelAppend: OptionalFieldLabel, + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', + { + defaultMessage: 'Adds time to the look-back period to prevent missed alerts.', + } + ), + }, +}; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/translations.tsx new file mode 100644 index 0000000000000..d49e7396f4c87 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/translations.tsx @@ -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 { i18n } from '@kbn/i18n'; + +export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle', + { + defaultMessage: 'Create rule without activating it', + } +); + +export const COMPLETE_WITH_ACTIVATING = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle', + { + defaultMessage: 'Create & activate rule', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.tsx rename to x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/user_info/index.test.tsx new file mode 100644 index 0000000000000..1d0f0e2e24f77 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/user_info/index.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useUserInfo } from './index'; + +import { usePrivilegeUser } from '../../containers/detection_engine/alerts/use_privilege_user'; +import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index'; +import { useKibana } from '../../../common/lib/kibana'; +jest.mock('../../containers/detection_engine/alerts/use_privilege_user'); +jest.mock('../../containers/detection_engine/alerts/use_signal_index'); +jest.mock('../../../common/lib/kibana'); + +describe('useUserInfo', () => { + beforeAll(() => { + (usePrivilegeUser as jest.Mock).mockReturnValue({}); + (useSignalIndex as jest.Mock).mockReturnValue({}); + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + securitySolution: { + crud: true, + }, + }, + }, + }, + }); + }); + it('returns default state', () => { + const { result } = renderHook(() => useUserInfo()); + + expect(result).toEqual({ + current: { + canUserCRUD: null, + hasEncryptionKey: null, + hasIndexManage: null, + hasIndexWrite: null, + isAuthenticated: null, + isSignalIndexExists: null, + loading: true, + signalIndexName: null, + }, + error: undefined, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/alerts/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/user_info/index.tsx new file mode 100644 index 0000000000000..8753064751f76 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/user_info/index.tsx @@ -0,0 +1,245 @@ +/* + * 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 { noop } from 'lodash/fp'; +import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react'; + +import { usePrivilegeUser } from '../../containers/detection_engine/alerts/use_privilege_user'; +import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index'; +import { useKibana } from '../../../common/lib/kibana'; + +export interface State { + canUserCRUD: boolean | null; + hasIndexManage: boolean | null; + hasIndexWrite: boolean | null; + isSignalIndexExists: boolean | null; + isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; + loading: boolean; + signalIndexName: string | null; +} + +const initialState: State = { + canUserCRUD: null, + hasIndexManage: null, + hasIndexWrite: null, + isSignalIndexExists: null, + isAuthenticated: null, + hasEncryptionKey: null, + loading: true, + signalIndexName: null, +}; + +export type Action = + | { type: 'updateLoading'; loading: boolean } + | { + type: 'updateHasIndexManage'; + hasIndexManage: boolean | null; + } + | { + type: 'updateHasIndexWrite'; + hasIndexWrite: boolean | null; + } + | { + type: 'updateIsSignalIndexExists'; + isSignalIndexExists: boolean | null; + } + | { + type: 'updateIsAuthenticated'; + isAuthenticated: boolean | null; + } + | { + type: 'updateHasEncryptionKey'; + hasEncryptionKey: boolean | null; + } + | { + type: 'updateCanUserCRUD'; + canUserCRUD: boolean | null; + } + | { + type: 'updateSignalIndexName'; + signalIndexName: string | null; + }; + +export const userInfoReducer = (state: State, action: Action): State => { + switch (action.type) { + case 'updateLoading': { + return { + ...state, + loading: action.loading, + }; + } + case 'updateHasIndexManage': { + return { + ...state, + hasIndexManage: action.hasIndexManage, + }; + } + case 'updateHasIndexWrite': { + return { + ...state, + hasIndexWrite: action.hasIndexWrite, + }; + } + case 'updateIsSignalIndexExists': { + return { + ...state, + isSignalIndexExists: action.isSignalIndexExists, + }; + } + case 'updateIsAuthenticated': { + return { + ...state, + isAuthenticated: action.isAuthenticated, + }; + } + case 'updateHasEncryptionKey': { + return { + ...state, + hasEncryptionKey: action.hasEncryptionKey, + }; + } + case 'updateCanUserCRUD': { + return { + ...state, + canUserCRUD: action.canUserCRUD, + }; + } + case 'updateSignalIndexName': { + return { + ...state, + signalIndexName: action.signalIndexName, + }; + } + default: + return state; + } +}; + +const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); + +const useUserData = () => useContext(StateUserInfoContext); + +interface ManageUserInfoProps { + children: React.ReactNode; +} + +export const ManageUserInfo = ({ children }: ManageUserInfoProps) => ( + + {children} + +); + +export const useUserInfo = (): State => { + const [ + { + canUserCRUD, + hasIndexManage, + hasIndexWrite, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + loading, + signalIndexName, + }, + dispatch, + ] = useUserData(); + const { + loading: privilegeLoading, + isAuthenticated: isApiAuthenticated, + hasEncryptionKey: isApiEncryptionKey, + hasIndexManage: hasApiIndexManage, + hasIndexWrite: hasApiIndexWrite, + } = usePrivilegeUser(); + const { + loading: indexNameLoading, + signalIndexExists: isApiSignalIndexExists, + signalIndexName: apiSignalIndexName, + createDeSignalIndex: createSignalIndex, + } = useSignalIndex(); + + const uiCapabilities = useKibana().services.application.capabilities; + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities.securitySolution.crud === 'boolean' + ? uiCapabilities.securitySolution.crud + : false; + + useEffect(() => { + if (loading !== privilegeLoading || indexNameLoading) { + dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading }); + } + }, [loading, privilegeLoading, indexNameLoading]); + + useEffect(() => { + if (!loading && hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) { + dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage }); + } + }, [loading, hasIndexManage, hasApiIndexManage]); + + useEffect(() => { + if (!loading && hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) { + dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite }); + } + }, [loading, hasIndexWrite, hasApiIndexWrite]); + + useEffect(() => { + if ( + !loading && + isSignalIndexExists !== isApiSignalIndexExists && + isApiSignalIndexExists != null + ) { + dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists }); + } + }, [loading, isSignalIndexExists, isApiSignalIndexExists]); + + useEffect(() => { + if (!loading && isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) { + dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated }); + } + }, [loading, isAuthenticated, isApiAuthenticated]); + + useEffect(() => { + if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) { + dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey }); + } + }, [loading, hasEncryptionKey, isApiEncryptionKey]); + + useEffect(() => { + if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { + dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); + } + }, [loading, canUserCRUD, capabilitiesCanUserCRUD]); + + useEffect(() => { + if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { + dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); + } + }, [loading, signalIndexName, apiSignalIndexName]); + + useEffect(() => { + if ( + isAuthenticated && + hasEncryptionKey && + hasIndexManage && + isSignalIndexExists != null && + !isSignalIndexExists && + createSignalIndex != null + ) { + createSignalIndex(); + } + }, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]); + + return { + loading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexManage, + hasIndexWrite, + signalIndexName, + }; +}; diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.test.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.test.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.test.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.test.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/mock.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/mock.ts diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/translations.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/translations.ts new file mode 100644 index 0000000000000..41f6a129d1b5e --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALERT_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.alerts.errorFetchingAlertsDescription', + { + defaultMessage: 'Failed to query alerts', + } +); + +export const PRIVILEGE_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.alerts.errorFetchingAlertsDescription', + { + defaultMessage: 'Failed to query alerts', + } +); + +export const SIGNAL_GET_NAME_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.alerts.errorGetAlertDescription', + { + defaultMessage: 'Failed to get signal index name', + } +); + +export const SIGNAL_POST_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.alerts.errorPostAlertDescription', + { + defaultMessage: 'Failed to create signal index', + } +); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/types.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/types.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.test.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/index.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/index.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/index.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/mock.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/translations.ts new file mode 100644 index 0000000000000..2b4b32bce9c7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/translations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULE_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.rules', + { + defaultMessage: 'Failed to fetch Rules', + } +); + +export const RULE_ADD_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.addRuleFailDescription', + { + defaultMessage: 'Failed to add Rule', + } +); + +export const RULE_PREPACKAGED_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleFailDescription', + { + defaultMessage: 'Failed to installed pre-packaged rules from elastic', + } +); + +export const RULE_PREPACKAGED_SUCCESS = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription', + { + defaultMessage: 'Installed pre-packaged rules from elastic', + } +); + +export const TAG_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.tagFetchFailDescription', + { + defaultMessage: 'Failed to fetch Tags', + } +); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.test.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.tsx rename to x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.tsx diff --git a/x-pack/plugins/siem/public/alerts/index.ts b/x-pack/plugins/security_solution/public/alerts/index.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/index.ts rename to x-pack/plugins/security_solution/public/alerts/index.ts diff --git a/x-pack/plugins/siem/public/alerts/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/alerts/mitre/mitre_tactics_techniques.ts similarity index 79% rename from x-pack/plugins/siem/public/alerts/mitre/mitre_tactics_techniques.ts rename to x-pack/plugins/security_solution/public/alerts/mitre/mitre_tactics_techniques.ts index 16ab73365222b..fb8deeec8309c 100644 --- a/x-pack/plugins/siem/public/alerts/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/alerts/mitre/mitre_tactics_techniques.ts @@ -76,9 +76,12 @@ export const tacticsOptions: MitreTacticsOptions[] = [ id: 'TA0009', name: 'Collection', reference: 'https://attack.mitre.org/tactics/TA0009', - text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.collectionDescription', { - defaultMessage: 'Collection (TA0009)', - }), + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.collectionDescription', + { + defaultMessage: 'Collection (TA0009)', + } + ), value: 'collection', }, { @@ -86,7 +89,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ name: 'Command and Control', reference: 'https://attack.mitre.org/tactics/TA0011', text: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTactics.commandAndControlDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.commandAndControlDescription', { defaultMessage: 'Command and Control (TA0011)' } ), value: 'commandAndControl', @@ -96,7 +99,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ name: 'Credential Access', reference: 'https://attack.mitre.org/tactics/TA0006', text: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTactics.credentialAccessDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.credentialAccessDescription', { defaultMessage: 'Credential Access (TA0006)' } ), value: 'credentialAccess', @@ -106,7 +109,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ name: 'Defense Evasion', reference: 'https://attack.mitre.org/tactics/TA0005', text: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTactics.defenseEvasionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.defenseEvasionDescription', { defaultMessage: 'Defense Evasion (TA0005)' } ), value: 'defenseEvasion', @@ -115,45 +118,60 @@ export const tacticsOptions: MitreTacticsOptions[] = [ id: 'TA0007', name: 'Discovery', reference: 'https://attack.mitre.org/tactics/TA0007', - text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.discoveryDescription', { - defaultMessage: 'Discovery (TA0007)', - }), + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.discoveryDescription', + { + defaultMessage: 'Discovery (TA0007)', + } + ), value: 'discovery', }, { id: 'TA0002', name: 'Execution', reference: 'https://attack.mitre.org/tactics/TA0002', - text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.executionDescription', { - defaultMessage: 'Execution (TA0002)', - }), + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.executionDescription', + { + defaultMessage: 'Execution (TA0002)', + } + ), value: 'execution', }, { id: 'TA0010', name: 'Exfiltration', reference: 'https://attack.mitre.org/tactics/TA0010', - text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.exfiltrationDescription', { - defaultMessage: 'Exfiltration (TA0010)', - }), + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.exfiltrationDescription', + { + defaultMessage: 'Exfiltration (TA0010)', + } + ), value: 'exfiltration', }, { id: 'TA0040', name: 'Impact', reference: 'https://attack.mitre.org/tactics/TA0040', - text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.impactDescription', { - defaultMessage: 'Impact (TA0040)', - }), + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.impactDescription', + { + defaultMessage: 'Impact (TA0040)', + } + ), value: 'impact', }, { id: 'TA0001', name: 'Initial Access', reference: 'https://attack.mitre.org/tactics/TA0001', - text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.initialAccessDescription', { - defaultMessage: 'Initial Access (TA0001)', - }), + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.initialAccessDescription', + { + defaultMessage: 'Initial Access (TA0001)', + } + ), value: 'initialAccess', }, { @@ -161,7 +179,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ name: 'Lateral Movement', reference: 'https://attack.mitre.org/tactics/TA0008', text: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTactics.lateralMovementDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.lateralMovementDescription', { defaultMessage: 'Lateral Movement (TA0008)' } ), value: 'lateralMovement', @@ -170,9 +188,12 @@ export const tacticsOptions: MitreTacticsOptions[] = [ id: 'TA0003', name: 'Persistence', reference: 'https://attack.mitre.org/tactics/TA0003', - text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.persistenceDescription', { - defaultMessage: 'Persistence (TA0003)', - }), + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.persistenceDescription', + { + defaultMessage: 'Persistence (TA0003)', + } + ), value: 'persistence', }, { @@ -180,7 +201,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ name: 'Privilege Escalation', reference: 'https://attack.mitre.org/tactics/TA0004', text: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTactics.privilegeEscalationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.privilegeEscalationDescription', { defaultMessage: 'Privilege Escalation (TA0004)' } ), value: 'privilegeEscalation', @@ -1789,7 +1810,7 @@ export const technique = [ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.bashProfileAndBashrcDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashProfileAndBashrcDescription', { defaultMessage: '.bash_profile and .bashrc (T1156)' } ), id: 'T1156', @@ -1800,7 +1821,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.accessTokenManipulationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.accessTokenManipulationDescription', { defaultMessage: 'Access Token Manipulation (T1134)' } ), id: 'T1134', @@ -1811,7 +1832,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.accessibilityFeaturesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.accessibilityFeaturesDescription', { defaultMessage: 'Accessibility Features (T1015)' } ), id: 'T1015', @@ -1822,7 +1843,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.accountAccessRemovalDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountAccessRemovalDescription', { defaultMessage: 'Account Access Removal (T1531)' } ), id: 'T1531', @@ -1833,7 +1854,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.accountDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountDiscoveryDescription', { defaultMessage: 'Account Discovery (T1087)' } ), id: 'T1087', @@ -1844,7 +1865,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.accountManipulationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountManipulationDescription', { defaultMessage: 'Account Manipulation (T1098)' } ), id: 'T1098', @@ -1855,7 +1876,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.appCertDlLsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.appCertDlLsDescription', { defaultMessage: 'AppCert DLLs (T1182)' } ), id: 'T1182', @@ -1866,7 +1887,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.appInitDlLsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.appInitDlLsDescription', { defaultMessage: 'AppInit DLLs (T1103)' } ), id: 'T1103', @@ -1877,7 +1898,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.appleScriptDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.appleScriptDescription', { defaultMessage: 'AppleScript (T1155)' } ), id: 'T1155', @@ -1888,7 +1909,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationAccessTokenDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationAccessTokenDescription', { defaultMessage: 'Application Access Token (T1527)' } ), id: 'T1527', @@ -1899,7 +1920,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationDeploymentSoftwareDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationDeploymentSoftwareDescription', { defaultMessage: 'Application Deployment Software (T1017)' } ), id: 'T1017', @@ -1910,7 +1931,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationShimmingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationShimmingDescription', { defaultMessage: 'Application Shimming (T1138)' } ), id: 'T1138', @@ -1921,7 +1942,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationWindowDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationWindowDiscoveryDescription', { defaultMessage: 'Application Window Discovery (T1010)' } ), id: 'T1010', @@ -1932,7 +1953,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.audioCaptureDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.audioCaptureDescription', { defaultMessage: 'Audio Capture (T1123)' } ), id: 'T1123', @@ -1943,7 +1964,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.authenticationPackageDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.authenticationPackageDescription', { defaultMessage: 'Authentication Package (T1131)' } ), id: 'T1131', @@ -1954,7 +1975,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.automatedCollectionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.automatedCollectionDescription', { defaultMessage: 'Automated Collection (T1119)' } ), id: 'T1119', @@ -1965,7 +1986,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.automatedExfiltrationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.automatedExfiltrationDescription', { defaultMessage: 'Automated Exfiltration (T1020)' } ), id: 'T1020', @@ -1975,9 +1996,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'automatedExfiltration', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.bitsJobsDescription', { - defaultMessage: 'BITS Jobs (T1197)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bitsJobsDescription', + { + defaultMessage: 'BITS Jobs (T1197)', + } + ), id: 'T1197', name: 'BITS Jobs', reference: 'https://attack.mitre.org/techniques/T1197', @@ -1986,7 +2010,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.bashHistoryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashHistoryDescription', { defaultMessage: 'Bash History (T1139)' } ), id: 'T1139', @@ -1997,7 +2021,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.binaryPaddingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.binaryPaddingDescription', { defaultMessage: 'Binary Padding (T1009)' } ), id: 'T1009', @@ -2007,9 +2031,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'binaryPadding', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.bootkitDescription', { - defaultMessage: 'Bootkit (T1067)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootkitDescription', + { + defaultMessage: 'Bootkit (T1067)', + } + ), id: 'T1067', name: 'Bootkit', reference: 'https://attack.mitre.org/techniques/T1067', @@ -2018,7 +2045,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.browserBookmarkDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.browserBookmarkDiscoveryDescription', { defaultMessage: 'Browser Bookmark Discovery (T1217)' } ), id: 'T1217', @@ -2029,7 +2056,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.browserExtensionsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.browserExtensionsDescription', { defaultMessage: 'Browser Extensions (T1176)' } ), id: 'T1176', @@ -2040,7 +2067,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.bruteForceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bruteForceDescription', { defaultMessage: 'Brute Force (T1110)' } ), id: 'T1110', @@ -2051,7 +2078,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.bypassUserAccountControlDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bypassUserAccountControlDescription', { defaultMessage: 'Bypass User Account Control (T1088)' } ), id: 'T1088', @@ -2061,9 +2088,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'bypassUserAccountControl', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.cmstpDescription', { - defaultMessage: 'CMSTP (T1191)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cmstpDescription', + { + defaultMessage: 'CMSTP (T1191)', + } + ), id: 'T1191', name: 'CMSTP', reference: 'https://attack.mitre.org/techniques/T1191', @@ -2072,7 +2102,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.changeDefaultFileAssociationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.changeDefaultFileAssociationDescription', { defaultMessage: 'Change Default File Association (T1042)' } ), id: 'T1042', @@ -2083,7 +2113,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.clearCommandHistoryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.clearCommandHistoryDescription', { defaultMessage: 'Clear Command History (T1146)' } ), id: 'T1146', @@ -2094,7 +2124,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.clipboardDataDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.clipboardDataDescription', { defaultMessage: 'Clipboard Data (T1115)' } ), id: 'T1115', @@ -2105,7 +2135,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.cloudInstanceMetadataApiDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudInstanceMetadataApiDescription', { defaultMessage: 'Cloud Instance Metadata API (T1522)' } ), id: 'T1522', @@ -2116,7 +2146,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.cloudServiceDashboardDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudServiceDashboardDescription', { defaultMessage: 'Cloud Service Dashboard (T1538)' } ), id: 'T1538', @@ -2127,7 +2157,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.cloudServiceDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudServiceDiscoveryDescription', { defaultMessage: 'Cloud Service Discovery (T1526)' } ), id: 'T1526', @@ -2138,7 +2168,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.codeSigningDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.codeSigningDescription', { defaultMessage: 'Code Signing (T1116)' } ), id: 'T1116', @@ -2149,7 +2179,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.commandLineInterfaceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.commandLineInterfaceDescription', { defaultMessage: 'Command-Line Interface (T1059)' } ), id: 'T1059', @@ -2160,7 +2190,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.commonlyUsedPortDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.commonlyUsedPortDescription', { defaultMessage: 'Commonly Used Port (T1043)' } ), id: 'T1043', @@ -2171,7 +2201,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.communicationThroughRemovableMediaDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.communicationThroughRemovableMediaDescription', { defaultMessage: 'Communication Through Removable Media (T1092)' } ), id: 'T1092', @@ -2182,7 +2212,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.compileAfterDeliveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.compileAfterDeliveryDescription', { defaultMessage: 'Compile After Delivery (T1500)' } ), id: 'T1500', @@ -2193,7 +2223,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.compiledHtmlFileDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.compiledHtmlFileDescription', { defaultMessage: 'Compiled HTML File (T1223)' } ), id: 'T1223', @@ -2204,7 +2234,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.componentFirmwareDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentFirmwareDescription', { defaultMessage: 'Component Firmware (T1109)' } ), id: 'T1109', @@ -2215,7 +2245,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.componentObjectModelHijackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelHijackingDescription', { defaultMessage: 'Component Object Model Hijacking (T1122)' } ), id: 'T1122', @@ -2226,7 +2256,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.componentObjectModelAndDistributedComDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelAndDistributedComDescription', { defaultMessage: 'Component Object Model and Distributed COM (T1175)' } ), id: 'T1175', @@ -2237,7 +2267,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.connectionProxyDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.connectionProxyDescription', { defaultMessage: 'Connection Proxy (T1090)' } ), id: 'T1090', @@ -2248,7 +2278,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.controlPanelItemsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.controlPanelItemsDescription', { defaultMessage: 'Control Panel Items (T1196)' } ), id: 'T1196', @@ -2259,7 +2289,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.createAccountDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.createAccountDescription', { defaultMessage: 'Create Account (T1136)' } ), id: 'T1136', @@ -2270,7 +2300,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialDumpingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialDumpingDescription', { defaultMessage: 'Credential Dumping (T1003)' } ), id: 'T1003', @@ -2281,7 +2311,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialsFromWebBrowsersDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsFromWebBrowsersDescription', { defaultMessage: 'Credentials from Web Browsers (T1503)' } ), id: 'T1503', @@ -2292,7 +2322,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialsInFilesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInFilesDescription', { defaultMessage: 'Credentials in Files (T1081)' } ), id: 'T1081', @@ -2303,7 +2333,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialsInRegistryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInRegistryDescription', { defaultMessage: 'Credentials in Registry (T1214)' } ), id: 'T1214', @@ -2314,7 +2344,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.customCommandAndControlProtocolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCommandAndControlProtocolDescription', { defaultMessage: 'Custom Command and Control Protocol (T1094)' } ), id: 'T1094', @@ -2325,7 +2355,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.customCryptographicProtocolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCryptographicProtocolDescription', { defaultMessage: 'Custom Cryptographic Protocol (T1024)' } ), id: 'T1024', @@ -2335,9 +2365,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'customCryptographicProtocol', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.dcShadowDescription', { - defaultMessage: 'DCShadow (T1207)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dcShadowDescription', + { + defaultMessage: 'DCShadow (T1207)', + } + ), id: 'T1207', name: 'DCShadow', reference: 'https://attack.mitre.org/techniques/T1207', @@ -2346,7 +2379,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dllSearchOrderHijackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSearchOrderHijackingDescription', { defaultMessage: 'DLL Search Order Hijacking (T1038)' } ), id: 'T1038', @@ -2357,7 +2390,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dllSideLoadingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSideLoadingDescription', { defaultMessage: 'DLL Side-Loading (T1073)' } ), id: 'T1073', @@ -2368,7 +2401,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataCompressedDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataCompressedDescription', { defaultMessage: 'Data Compressed (T1002)' } ), id: 'T1002', @@ -2379,7 +2412,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataDestructionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataDestructionDescription', { defaultMessage: 'Data Destruction (T1485)' } ), id: 'T1485', @@ -2390,7 +2423,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataEncodingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncodingDescription', { defaultMessage: 'Data Encoding (T1132)' } ), id: 'T1132', @@ -2401,7 +2434,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataEncryptedDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedDescription', { defaultMessage: 'Data Encrypted (T1022)' } ), id: 'T1022', @@ -2412,7 +2445,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataEncryptedForImpactDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedForImpactDescription', { defaultMessage: 'Data Encrypted for Impact (T1486)' } ), id: 'T1486', @@ -2423,7 +2456,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataObfuscationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataObfuscationDescription', { defaultMessage: 'Data Obfuscation (T1001)' } ), id: 'T1001', @@ -2434,7 +2467,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataStagedDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataStagedDescription', { defaultMessage: 'Data Staged (T1074)' } ), id: 'T1074', @@ -2445,7 +2478,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataTransferSizeLimitsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataTransferSizeLimitsDescription', { defaultMessage: 'Data Transfer Size Limits (T1030)' } ), id: 'T1030', @@ -2456,7 +2489,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromCloudStorageObjectDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromCloudStorageObjectDescription', { defaultMessage: 'Data from Cloud Storage Object (T1530)' } ), id: 'T1530', @@ -2467,7 +2500,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromInformationRepositoriesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromInformationRepositoriesDescription', { defaultMessage: 'Data from Information Repositories (T1213)' } ), id: 'T1213', @@ -2478,7 +2511,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromLocalSystemDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromLocalSystemDescription', { defaultMessage: 'Data from Local System (T1005)' } ), id: 'T1005', @@ -2489,7 +2522,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromNetworkSharedDriveDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromNetworkSharedDriveDescription', { defaultMessage: 'Data from Network Shared Drive (T1039)' } ), id: 'T1039', @@ -2500,7 +2533,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromRemovableMediaDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromRemovableMediaDescription', { defaultMessage: 'Data from Removable Media (T1025)' } ), id: 'T1025', @@ -2511,7 +2544,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.defacementDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.defacementDescription', { defaultMessage: 'Defacement (T1491)' } ), id: 'T1491', @@ -2522,7 +2555,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.deobfuscateDecodeFilesOrInformationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.deobfuscateDecodeFilesOrInformationDescription', { defaultMessage: 'Deobfuscate/Decode Files or Information (T1140)' } ), id: 'T1140', @@ -2533,7 +2566,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.disablingSecurityToolsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.disablingSecurityToolsDescription', { defaultMessage: 'Disabling Security Tools (T1089)' } ), id: 'T1089', @@ -2544,7 +2577,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.diskContentWipeDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskContentWipeDescription', { defaultMessage: 'Disk Content Wipe (T1488)' } ), id: 'T1488', @@ -2555,7 +2588,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.diskStructureWipeDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskStructureWipeDescription', { defaultMessage: 'Disk Structure Wipe (T1487)' } ), id: 'T1487', @@ -2566,7 +2599,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.domainFrontingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainFrontingDescription', { defaultMessage: 'Domain Fronting (T1172)' } ), id: 'T1172', @@ -2577,7 +2610,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.domainGenerationAlgorithmsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainGenerationAlgorithmsDescription', { defaultMessage: 'Domain Generation Algorithms (T1483)' } ), id: 'T1483', @@ -2588,7 +2621,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.domainTrustDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainTrustDiscoveryDescription', { defaultMessage: 'Domain Trust Discovery (T1482)' } ), id: 'T1482', @@ -2599,7 +2632,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.driveByCompromiseDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.driveByCompromiseDescription', { defaultMessage: 'Drive-by Compromise (T1189)' } ), id: 'T1189', @@ -2610,7 +2643,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dylibHijackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dylibHijackingDescription', { defaultMessage: 'Dylib Hijacking (T1157)' } ), id: 'T1157', @@ -2621,7 +2654,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.dynamicDataExchangeDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dynamicDataExchangeDescription', { defaultMessage: 'Dynamic Data Exchange (T1173)' } ), id: 'T1173', @@ -2632,7 +2665,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.elevatedExecutionWithPromptDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.elevatedExecutionWithPromptDescription', { defaultMessage: 'Elevated Execution with Prompt (T1514)' } ), id: 'T1514', @@ -2643,7 +2676,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.emailCollectionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.emailCollectionDescription', { defaultMessage: 'Email Collection (T1114)' } ), id: 'T1114', @@ -2653,9 +2686,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'emailCollection', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.emondDescription', { - defaultMessage: 'Emond (T1519)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.emondDescription', + { + defaultMessage: 'Emond (T1519)', + } + ), id: 'T1519', name: 'Emond', reference: 'https://attack.mitre.org/techniques/T1519', @@ -2664,7 +2700,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.endpointDenialOfServiceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.endpointDenialOfServiceDescription', { defaultMessage: 'Endpoint Denial of Service (T1499)' } ), id: 'T1499', @@ -2675,7 +2711,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.executionGuardrailsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionGuardrailsDescription', { defaultMessage: 'Execution Guardrails (T1480)' } ), id: 'T1480', @@ -2686,7 +2722,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.executionThroughApiDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughApiDescription', { defaultMessage: 'Execution through API (T1106)' } ), id: 'T1106', @@ -2697,7 +2733,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.executionThroughModuleLoadDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughModuleLoadDescription', { defaultMessage: 'Execution through Module Load (T1129)' } ), id: 'T1129', @@ -2708,7 +2744,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverAlternativeProtocolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverAlternativeProtocolDescription', { defaultMessage: 'Exfiltration Over Alternative Protocol (T1048)' } ), id: 'T1048', @@ -2719,7 +2755,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverCommandAndControlChannelDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverCommandAndControlChannelDescription', { defaultMessage: 'Exfiltration Over Command and Control Channel (T1041)' } ), id: 'T1041', @@ -2730,7 +2766,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverOtherNetworkMediumDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverOtherNetworkMediumDescription', { defaultMessage: 'Exfiltration Over Other Network Medium (T1011)' } ), id: 'T1011', @@ -2741,7 +2777,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverPhysicalMediumDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverPhysicalMediumDescription', { defaultMessage: 'Exfiltration Over Physical Medium (T1052)' } ), id: 'T1052', @@ -2752,7 +2788,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitPublicFacingApplicationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitPublicFacingApplicationDescription', { defaultMessage: 'Exploit Public-Facing Application (T1190)' } ), id: 'T1190', @@ -2763,7 +2799,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForClientExecutionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationForClientExecutionDescription', { defaultMessage: 'Exploitation for Client Execution (T1203)' } ), id: 'T1203', @@ -2774,7 +2810,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForCredentialAccessDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationForCredentialAccessDescription', { defaultMessage: 'Exploitation for Credential Access (T1212)' } ), id: 'T1212', @@ -2785,7 +2821,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForDefenseEvasionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationForDefenseEvasionDescription', { defaultMessage: 'Exploitation for Defense Evasion (T1211)' } ), id: 'T1211', @@ -2796,7 +2832,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForPrivilegeEscalationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationForPrivilegeEscalationDescription', { defaultMessage: 'Exploitation for Privilege Escalation (T1068)' } ), id: 'T1068', @@ -2807,7 +2843,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationOfRemoteServicesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationOfRemoteServicesDescription', { defaultMessage: 'Exploitation of Remote Services (T1210)' } ), id: 'T1210', @@ -2818,7 +2854,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.externalRemoteServicesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.externalRemoteServicesDescription', { defaultMessage: 'External Remote Services (T1133)' } ), id: 'T1133', @@ -2829,7 +2865,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.extraWindowMemoryInjectionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.extraWindowMemoryInjectionDescription', { defaultMessage: 'Extra Window Memory Injection (T1181)' } ), id: 'T1181', @@ -2840,7 +2876,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.fallbackChannelsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fallbackChannelsDescription', { defaultMessage: 'Fallback Channels (T1008)' } ), id: 'T1008', @@ -2851,7 +2887,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.fileDeletionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileDeletionDescription', { defaultMessage: 'File Deletion (T1107)' } ), id: 'T1107', @@ -2862,7 +2898,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.fileSystemLogicalOffsetsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemLogicalOffsetsDescription', { defaultMessage: 'File System Logical Offsets (T1006)' } ), id: 'T1006', @@ -2873,7 +2909,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.fileSystemPermissionsWeaknessDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemPermissionsWeaknessDescription', { defaultMessage: 'File System Permissions Weakness (T1044)' } ), id: 'T1044', @@ -2884,7 +2920,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.fileAndDirectoryDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileAndDirectoryDiscoveryDescription', { defaultMessage: 'File and Directory Discovery (T1083)' } ), id: 'T1083', @@ -2895,7 +2931,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.fileAndDirectoryPermissionsModificationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileAndDirectoryPermissionsModificationDescription', { defaultMessage: 'File and Directory Permissions Modification (T1222)' } ), id: 'T1222', @@ -2906,7 +2942,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.firmwareCorruptionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.firmwareCorruptionDescription', { defaultMessage: 'Firmware Corruption (T1495)' } ), id: 'T1495', @@ -2917,7 +2953,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.forcedAuthenticationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.forcedAuthenticationDescription', { defaultMessage: 'Forced Authentication (T1187)' } ), id: 'T1187', @@ -2928,7 +2964,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.gatekeeperBypassDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatekeeperBypassDescription', { defaultMessage: 'Gatekeeper Bypass (T1144)' } ), id: 'T1144', @@ -2939,7 +2975,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.graphicalUserInterfaceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.graphicalUserInterfaceDescription', { defaultMessage: 'Graphical User Interface (T1061)' } ), id: 'T1061', @@ -2950,7 +2986,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.groupPolicyModificationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.groupPolicyModificationDescription', { defaultMessage: 'Group Policy Modification (T1484)' } ), id: 'T1484', @@ -2961,7 +2997,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.histcontrolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.histcontrolDescription', { defaultMessage: 'HISTCONTROL (T1148)' } ), id: 'T1148', @@ -2972,7 +3008,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.hardwareAdditionsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hardwareAdditionsDescription', { defaultMessage: 'Hardware Additions (T1200)' } ), id: 'T1200', @@ -2983,7 +3019,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.hiddenFilesAndDirectoriesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenFilesAndDirectoriesDescription', { defaultMessage: 'Hidden Files and Directories (T1158)' } ), id: 'T1158', @@ -2994,7 +3030,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.hiddenUsersDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenUsersDescription', { defaultMessage: 'Hidden Users (T1147)' } ), id: 'T1147', @@ -3005,7 +3041,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.hiddenWindowDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenWindowDescription', { defaultMessage: 'Hidden Window (T1143)' } ), id: 'T1143', @@ -3015,9 +3051,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'hiddenWindow', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.hookingDescription', { - defaultMessage: 'Hooking (T1179)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hookingDescription', + { + defaultMessage: 'Hooking (T1179)', + } + ), id: 'T1179', name: 'Hooking', reference: 'https://attack.mitre.org/techniques/T1179', @@ -3026,7 +3065,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.hypervisorDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hypervisorDescription', { defaultMessage: 'Hypervisor (T1062)' } ), id: 'T1062', @@ -3037,7 +3076,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.imageFileExecutionOptionsInjectionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.imageFileExecutionOptionsInjectionDescription', { defaultMessage: 'Image File Execution Options Injection (T1183)' } ), id: 'T1183', @@ -3048,7 +3087,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.implantContainerImageDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription', { defaultMessage: 'Implant Container Image (T1525)' } ), id: 'T1525', @@ -3059,7 +3098,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.indicatorBlockingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorBlockingDescription', { defaultMessage: 'Indicator Blocking (T1054)' } ), id: 'T1054', @@ -3070,7 +3109,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.indicatorRemovalFromToolsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalFromToolsDescription', { defaultMessage: 'Indicator Removal from Tools (T1066)' } ), id: 'T1066', @@ -3081,7 +3120,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription', { defaultMessage: 'Indicator Removal on Host (T1070)' } ), id: 'T1070', @@ -3092,7 +3131,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription', { defaultMessage: 'Indirect Command Execution (T1202)' } ), id: 'T1202', @@ -3103,7 +3142,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.inhibitSystemRecoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.inhibitSystemRecoveryDescription', { defaultMessage: 'Inhibit System Recovery (T1490)' } ), id: 'T1490', @@ -3114,7 +3153,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.inputCaptureDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.inputCaptureDescription', { defaultMessage: 'Input Capture (T1056)' } ), id: 'T1056', @@ -3125,7 +3164,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.inputPromptDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.inputPromptDescription', { defaultMessage: 'Input Prompt (T1141)' } ), id: 'T1141', @@ -3136,7 +3175,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.installRootCertificateDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.installRootCertificateDescription', { defaultMessage: 'Install Root Certificate (T1130)' } ), id: 'T1130', @@ -3147,7 +3186,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.installUtilDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.installUtilDescription', { defaultMessage: 'InstallUtil (T1118)' } ), id: 'T1118', @@ -3158,7 +3197,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.internalSpearphishingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.internalSpearphishingDescription', { defaultMessage: 'Internal Spearphishing (T1534)' } ), id: 'T1534', @@ -3169,7 +3208,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.kerberoastingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.kerberoastingDescription', { defaultMessage: 'Kerberoasting (T1208)' } ), id: 'T1208', @@ -3180,7 +3219,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.kernelModulesAndExtensionsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.kernelModulesAndExtensionsDescription', { defaultMessage: 'Kernel Modules and Extensions (T1215)' } ), id: 'T1215', @@ -3190,9 +3229,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'kernelModulesAndExtensions', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.keychainDescription', { - defaultMessage: 'Keychain (T1142)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.keychainDescription', + { + defaultMessage: 'Keychain (T1142)', + } + ), id: 'T1142', name: 'Keychain', reference: 'https://attack.mitre.org/techniques/T1142', @@ -3201,7 +3243,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.lcLoadDylibAdditionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcLoadDylibAdditionDescription', { defaultMessage: 'LC_LOAD_DYLIB Addition (T1161)' } ), id: 'T1161', @@ -3212,7 +3254,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.lcMainHijackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcMainHijackingDescription', { defaultMessage: 'LC_MAIN Hijacking (T1149)' } ), id: 'T1149', @@ -3223,7 +3265,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.llmnrNbtNsPoisoningAndRelayDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.llmnrNbtNsPoisoningAndRelayDescription', { defaultMessage: 'LLMNR/NBT-NS Poisoning and Relay (T1171)' } ), id: 'T1171', @@ -3234,7 +3276,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.lsassDriverDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.lsassDriverDescription', { defaultMessage: 'LSASS Driver (T1177)' } ), id: 'T1177', @@ -3245,7 +3287,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.launchAgentDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchAgentDescription', { defaultMessage: 'Launch Agent (T1159)' } ), id: 'T1159', @@ -3256,7 +3298,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.launchDaemonDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchDaemonDescription', { defaultMessage: 'Launch Daemon (T1160)' } ), id: 'T1160', @@ -3266,9 +3308,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'launchDaemon', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.launchctlDescription', { - defaultMessage: 'Launchctl (T1152)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchctlDescription', + { + defaultMessage: 'Launchctl (T1152)', + } + ), id: 'T1152', name: 'Launchctl', reference: 'https://attack.mitre.org/techniques/T1152', @@ -3277,7 +3322,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.localJobSchedulingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.localJobSchedulingDescription', { defaultMessage: 'Local Job Scheduling (T1168)' } ), id: 'T1168', @@ -3287,9 +3332,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'localJobScheduling', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.loginItemDescription', { - defaultMessage: 'Login Item (T1162)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.loginItemDescription', + { + defaultMessage: 'Login Item (T1162)', + } + ), id: 'T1162', name: 'Login Item', reference: 'https://attack.mitre.org/techniques/T1162', @@ -3298,7 +3346,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.logonScriptsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.logonScriptsDescription', { defaultMessage: 'Logon Scripts (T1037)' } ), id: 'T1037', @@ -3309,7 +3357,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.manInTheBrowserDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.manInTheBrowserDescription', { defaultMessage: 'Man in the Browser (T1185)' } ), id: 'T1185', @@ -3320,7 +3368,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.masqueradingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.masqueradingDescription', { defaultMessage: 'Masquerading (T1036)' } ), id: 'T1036', @@ -3331,7 +3379,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.modifyExistingServiceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyExistingServiceDescription', { defaultMessage: 'Modify Existing Service (T1031)' } ), id: 'T1031', @@ -3342,7 +3390,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.modifyRegistryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyRegistryDescription', { defaultMessage: 'Modify Registry (T1112)' } ), id: 'T1112', @@ -3352,9 +3400,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'modifyRegistry', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.mshtaDescription', { - defaultMessage: 'Mshta (T1170)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.mshtaDescription', + { + defaultMessage: 'Mshta (T1170)', + } + ), id: 'T1170', name: 'Mshta', reference: 'https://attack.mitre.org/techniques/T1170', @@ -3363,7 +3414,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.multiStageChannelsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.multiStageChannelsDescription', { defaultMessage: 'Multi-Stage Channels (T1104)' } ), id: 'T1104', @@ -3374,7 +3425,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.multiHopProxyDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.multiHopProxyDescription', { defaultMessage: 'Multi-hop Proxy (T1188)' } ), id: 'T1188', @@ -3385,7 +3436,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.multibandCommunicationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.multibandCommunicationDescription', { defaultMessage: 'Multiband Communication (T1026)' } ), id: 'T1026', @@ -3396,7 +3447,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.multilayerEncryptionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.multilayerEncryptionDescription', { defaultMessage: 'Multilayer Encryption (T1079)' } ), id: 'T1079', @@ -3407,7 +3458,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.ntfsFileAttributesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.ntfsFileAttributesDescription', { defaultMessage: 'NTFS File Attributes (T1096)' } ), id: 'T1096', @@ -3418,7 +3469,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.netshHelperDllDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.netshHelperDllDescription', { defaultMessage: 'Netsh Helper DLL (T1128)' } ), id: 'T1128', @@ -3429,7 +3480,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.networkDenialOfServiceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkDenialOfServiceDescription', { defaultMessage: 'Network Denial of Service (T1498)' } ), id: 'T1498', @@ -3440,7 +3491,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.networkServiceScanningDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkServiceScanningDescription', { defaultMessage: 'Network Service Scanning (T1046)' } ), id: 'T1046', @@ -3451,7 +3502,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.networkShareConnectionRemovalDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareConnectionRemovalDescription', { defaultMessage: 'Network Share Connection Removal (T1126)' } ), id: 'T1126', @@ -3462,7 +3513,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.networkShareDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareDiscoveryDescription', { defaultMessage: 'Network Share Discovery (T1135)' } ), id: 'T1135', @@ -3473,7 +3524,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.networkSniffingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkSniffingDescription', { defaultMessage: 'Network Sniffing (T1040)' } ), id: 'T1040', @@ -3484,7 +3535,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.newServiceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.newServiceDescription', { defaultMessage: 'New Service (T1050)' } ), id: 'T1050', @@ -3495,7 +3546,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.obfuscatedFilesOrInformationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.obfuscatedFilesOrInformationDescription', { defaultMessage: 'Obfuscated Files or Information (T1027)' } ), id: 'T1027', @@ -3506,7 +3557,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.officeApplicationStartupDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.officeApplicationStartupDescription', { defaultMessage: 'Office Application Startup (T1137)' } ), id: 'T1137', @@ -3517,7 +3568,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.parentPidSpoofingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.parentPidSpoofingDescription', { defaultMessage: 'Parent PID Spoofing (T1502)' } ), id: 'T1502', @@ -3528,7 +3579,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.passTheHashDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheHashDescription', { defaultMessage: 'Pass the Hash (T1075)' } ), id: 'T1075', @@ -3539,7 +3590,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.passTheTicketDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheTicketDescription', { defaultMessage: 'Pass the Ticket (T1097)' } ), id: 'T1097', @@ -3550,7 +3601,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.passwordFilterDllDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordFilterDllDescription', { defaultMessage: 'Password Filter DLL (T1174)' } ), id: 'T1174', @@ -3561,7 +3612,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.passwordPolicyDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordPolicyDiscoveryDescription', { defaultMessage: 'Password Policy Discovery (T1201)' } ), id: 'T1201', @@ -3572,7 +3623,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.pathInterceptionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.pathInterceptionDescription', { defaultMessage: 'Path Interception (T1034)' } ), id: 'T1034', @@ -3583,7 +3634,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.peripheralDeviceDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.peripheralDeviceDiscoveryDescription', { defaultMessage: 'Peripheral Device Discovery (T1120)' } ), id: 'T1120', @@ -3594,7 +3645,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.permissionGroupsDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.permissionGroupsDiscoveryDescription', { defaultMessage: 'Permission Groups Discovery (T1069)' } ), id: 'T1069', @@ -3605,7 +3656,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.plistModificationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.plistModificationDescription', { defaultMessage: 'Plist Modification (T1150)' } ), id: 'T1150', @@ -3616,7 +3667,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.portKnockingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.portKnockingDescription', { defaultMessage: 'Port Knocking (T1205)' } ), id: 'T1205', @@ -3627,7 +3678,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.portMonitorsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.portMonitorsDescription', { defaultMessage: 'Port Monitors (T1013)' } ), id: 'T1013', @@ -3638,7 +3689,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.powerShellDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellDescription', { defaultMessage: 'PowerShell (T1086)' } ), id: 'T1086', @@ -3649,7 +3700,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.powerShellProfileDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellProfileDescription', { defaultMessage: 'PowerShell Profile (T1504)' } ), id: 'T1504', @@ -3660,7 +3711,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.privateKeysDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.privateKeysDescription', { defaultMessage: 'Private Keys (T1145)' } ), id: 'T1145', @@ -3671,7 +3722,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.processDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.processDiscoveryDescription', { defaultMessage: 'Process Discovery (T1057)' } ), id: 'T1057', @@ -3682,7 +3733,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.processDoppelgangingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.processDoppelgangingDescription', { defaultMessage: 'Process Doppelgänging (T1186)' } ), id: 'T1186', @@ -3693,7 +3744,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.processHollowingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.processHollowingDescription', { defaultMessage: 'Process Hollowing (T1093)' } ), id: 'T1093', @@ -3704,7 +3755,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.processInjectionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.processInjectionDescription', { defaultMessage: 'Process Injection (T1055)' } ), id: 'T1055', @@ -3715,7 +3766,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.queryRegistryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.queryRegistryDescription', { defaultMessage: 'Query Registry (T1012)' } ), id: 'T1012', @@ -3725,9 +3776,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'queryRegistry', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.rcCommonDescription', { - defaultMessage: 'Rc.common (T1163)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rcCommonDescription', + { + defaultMessage: 'Rc.common (T1163)', + } + ), id: 'T1163', name: 'Rc.common', reference: 'https://attack.mitre.org/techniques/T1163', @@ -3736,7 +3790,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.reOpenedApplicationsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.reOpenedApplicationsDescription', { defaultMessage: 'Re-opened Applications (T1164)' } ), id: 'T1164', @@ -3747,7 +3801,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.redundantAccessDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.redundantAccessDescription', { defaultMessage: 'Redundant Access (T1108)' } ), id: 'T1108', @@ -3758,7 +3812,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.registryRunKeysStartupFolderDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.registryRunKeysStartupFolderDescription', { defaultMessage: 'Registry Run Keys / Startup Folder (T1060)' } ), id: 'T1060', @@ -3769,7 +3823,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.regsvcsRegasmDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvcsRegasmDescription', { defaultMessage: 'Regsvcs/Regasm (T1121)' } ), id: 'T1121', @@ -3779,9 +3833,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'regsvcsRegasm', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.regsvr32Description', { - defaultMessage: 'Regsvr32 (T1117)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvr32Description', + { + defaultMessage: 'Regsvr32 (T1117)', + } + ), id: 'T1117', name: 'Regsvr32', reference: 'https://attack.mitre.org/techniques/T1117', @@ -3790,7 +3847,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteAccessToolsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteAccessToolsDescription', { defaultMessage: 'Remote Access Tools (T1219)' } ), id: 'T1219', @@ -3801,7 +3858,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteDesktopProtocolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteDesktopProtocolDescription', { defaultMessage: 'Remote Desktop Protocol (T1076)' } ), id: 'T1076', @@ -3812,7 +3869,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteFileCopyDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteFileCopyDescription', { defaultMessage: 'Remote File Copy (T1105)' } ), id: 'T1105', @@ -3823,7 +3880,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteServicesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteServicesDescription', { defaultMessage: 'Remote Services (T1021)' } ), id: 'T1021', @@ -3834,7 +3891,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteSystemDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteSystemDiscoveryDescription', { defaultMessage: 'Remote System Discovery (T1018)' } ), id: 'T1018', @@ -3845,7 +3902,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.replicationThroughRemovableMediaDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.replicationThroughRemovableMediaDescription', { defaultMessage: 'Replication Through Removable Media (T1091)' } ), id: 'T1091', @@ -3856,7 +3913,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.resourceHijackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.resourceHijackingDescription', { defaultMessage: 'Resource Hijacking (T1496)' } ), id: 'T1496', @@ -3867,7 +3924,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.revertCloudInstanceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.revertCloudInstanceDescription', { defaultMessage: 'Revert Cloud Instance (T1536)' } ), id: 'T1536', @@ -3877,9 +3934,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'revertCloudInstance', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.rootkitDescription', { - defaultMessage: 'Rootkit (T1014)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rootkitDescription', + { + defaultMessage: 'Rootkit (T1014)', + } + ), id: 'T1014', name: 'Rootkit', reference: 'https://attack.mitre.org/techniques/T1014', @@ -3887,9 +3947,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'rootkit', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.rundll32Description', { - defaultMessage: 'Rundll32 (T1085)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rundll32Description', + { + defaultMessage: 'Rundll32 (T1085)', + } + ), id: 'T1085', name: 'Rundll32', reference: 'https://attack.mitre.org/techniques/T1085', @@ -3898,7 +3961,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.runtimeDataManipulationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.runtimeDataManipulationDescription', { defaultMessage: 'Runtime Data Manipulation (T1494)' } ), id: 'T1494', @@ -3909,7 +3972,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.sidHistoryInjectionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sidHistoryInjectionDescription', { defaultMessage: 'SID-History Injection (T1178)' } ), id: 'T1178', @@ -3920,7 +3983,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.sipAndTrustProviderHijackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sipAndTrustProviderHijackingDescription', { defaultMessage: 'SIP and Trust Provider Hijacking (T1198)' } ), id: 'T1198', @@ -3931,7 +3994,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.sshHijackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sshHijackingDescription', { defaultMessage: 'SSH Hijacking (T1184)' } ), id: 'T1184', @@ -3942,7 +4005,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.scheduledTaskDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTaskDescription', { defaultMessage: 'Scheduled Task (T1053)' } ), id: 'T1053', @@ -3953,7 +4016,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.scheduledTransferDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTransferDescription', { defaultMessage: 'Scheduled Transfer (T1029)' } ), id: 'T1029', @@ -3964,7 +4027,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.screenCaptureDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.screenCaptureDescription', { defaultMessage: 'Screen Capture (T1113)' } ), id: 'T1113', @@ -3975,7 +4038,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.screensaverDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.screensaverDescription', { defaultMessage: 'Screensaver (T1180)' } ), id: 'T1180', @@ -3985,9 +4048,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'screensaver', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.scriptingDescription', { - defaultMessage: 'Scripting (T1064)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scriptingDescription', + { + defaultMessage: 'Scripting (T1064)', + } + ), id: 'T1064', name: 'Scripting', reference: 'https://attack.mitre.org/techniques/T1064', @@ -3996,7 +4062,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.securitySoftwareDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySoftwareDiscoveryDescription', { defaultMessage: 'Security Software Discovery (T1063)' } ), id: 'T1063', @@ -4007,7 +4073,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.securitySupportProviderDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySupportProviderDescription', { defaultMessage: 'Security Support Provider (T1101)' } ), id: 'T1101', @@ -4018,7 +4084,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.securitydMemoryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitydMemoryDescription', { defaultMessage: 'Securityd Memory (T1167)' } ), id: 'T1167', @@ -4029,7 +4095,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.serverSoftwareComponentDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.serverSoftwareComponentDescription', { defaultMessage: 'Server Software Component (T1505)' } ), id: 'T1505', @@ -4040,7 +4106,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.serviceExecutionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceExecutionDescription', { defaultMessage: 'Service Execution (T1035)' } ), id: 'T1035', @@ -4051,7 +4117,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.serviceRegistryPermissionsWeaknessDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceRegistryPermissionsWeaknessDescription', { defaultMessage: 'Service Registry Permissions Weakness (T1058)' } ), id: 'T1058', @@ -4062,7 +4128,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.serviceStopDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceStopDescription', { defaultMessage: 'Service Stop (T1489)' } ), id: 'T1489', @@ -4073,7 +4139,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.setuidAndSetgidDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.setuidAndSetgidDescription', { defaultMessage: 'Setuid and Setgid (T1166)' } ), id: 'T1166', @@ -4084,7 +4150,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.sharedWebrootDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sharedWebrootDescription', { defaultMessage: 'Shared Webroot (T1051)' } ), id: 'T1051', @@ -4095,7 +4161,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.shortcutModificationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.shortcutModificationDescription', { defaultMessage: 'Shortcut Modification (T1023)' } ), id: 'T1023', @@ -4106,7 +4172,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.signedBinaryProxyExecutionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.signedBinaryProxyExecutionDescription', { defaultMessage: 'Signed Binary Proxy Execution (T1218)' } ), id: 'T1218', @@ -4117,7 +4183,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.signedScriptProxyExecutionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.signedScriptProxyExecutionDescription', { defaultMessage: 'Signed Script Proxy Execution (T1216)' } ), id: 'T1216', @@ -4128,7 +4194,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.softwareDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwareDiscoveryDescription', { defaultMessage: 'Software Discovery (T1518)' } ), id: 'T1518', @@ -4139,7 +4205,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.softwarePackingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwarePackingDescription', { defaultMessage: 'Software Packing (T1045)' } ), id: 'T1045', @@ -4149,9 +4215,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'softwarePacking', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.sourceDescription', { - defaultMessage: 'Source (T1153)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sourceDescription', + { + defaultMessage: 'Source (T1153)', + } + ), id: 'T1153', name: 'Source', reference: 'https://attack.mitre.org/techniques/T1153', @@ -4160,7 +4229,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.spaceAfterFilenameDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spaceAfterFilenameDescription', { defaultMessage: 'Space after Filename (T1151)' } ), id: 'T1151', @@ -4171,7 +4240,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.spearphishingAttachmentDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingAttachmentDescription', { defaultMessage: 'Spearphishing Attachment (T1193)' } ), id: 'T1193', @@ -4182,7 +4251,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.spearphishingLinkDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingLinkDescription', { defaultMessage: 'Spearphishing Link (T1192)' } ), id: 'T1192', @@ -4193,7 +4262,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.spearphishingViaServiceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingViaServiceDescription', { defaultMessage: 'Spearphishing via Service (T1194)' } ), id: 'T1194', @@ -4204,7 +4273,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.standardApplicationLayerProtocolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardApplicationLayerProtocolDescription', { defaultMessage: 'Standard Application Layer Protocol (T1071)' } ), id: 'T1071', @@ -4215,7 +4284,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.standardCryptographicProtocolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardCryptographicProtocolDescription', { defaultMessage: 'Standard Cryptographic Protocol (T1032)' } ), id: 'T1032', @@ -4226,7 +4295,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.standardNonApplicationLayerProtocolDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardNonApplicationLayerProtocolDescription', { defaultMessage: 'Standard Non-Application Layer Protocol (T1095)' } ), id: 'T1095', @@ -4237,7 +4306,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.startupItemsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.startupItemsDescription', { defaultMessage: 'Startup Items (T1165)' } ), id: 'T1165', @@ -4248,7 +4317,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription', { defaultMessage: 'Steal Application Access Token (T1528)' } ), id: 'T1528', @@ -4259,7 +4328,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.stealWebSessionCookieDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealWebSessionCookieDescription', { defaultMessage: 'Steal Web Session Cookie (T1539)' } ), id: 'T1539', @@ -4270,7 +4339,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.storedDataManipulationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.storedDataManipulationDescription', { defaultMessage: 'Stored Data Manipulation (T1492)' } ), id: 'T1492', @@ -4280,9 +4349,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'storedDataManipulation', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.sudoDescription', { - defaultMessage: 'Sudo (T1169)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoDescription', + { + defaultMessage: 'Sudo (T1169)', + } + ), id: 'T1169', name: 'Sudo', reference: 'https://attack.mitre.org/techniques/T1169', @@ -4291,7 +4363,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.sudoCachingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoCachingDescription', { defaultMessage: 'Sudo Caching (T1206)' } ), id: 'T1206', @@ -4302,7 +4374,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.supplyChainCompromiseDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.supplyChainCompromiseDescription', { defaultMessage: 'Supply Chain Compromise (T1195)' } ), id: 'T1195', @@ -4313,7 +4385,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemFirmwareDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemFirmwareDescription', { defaultMessage: 'System Firmware (T1019)' } ), id: 'T1019', @@ -4324,7 +4396,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemInformationDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemInformationDiscoveryDescription', { defaultMessage: 'System Information Discovery (T1082)' } ), id: 'T1082', @@ -4335,7 +4407,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemNetworkConfigurationDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConfigurationDiscoveryDescription', { defaultMessage: 'System Network Configuration Discovery (T1016)' } ), id: 'T1016', @@ -4346,7 +4418,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemNetworkConnectionsDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConnectionsDiscoveryDescription', { defaultMessage: 'System Network Connections Discovery (T1049)' } ), id: 'T1049', @@ -4357,7 +4429,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemOwnerUserDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemOwnerUserDiscoveryDescription', { defaultMessage: 'System Owner/User Discovery (T1033)' } ), id: 'T1033', @@ -4368,7 +4440,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemServiceDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemServiceDiscoveryDescription', { defaultMessage: 'System Service Discovery (T1007)' } ), id: 'T1007', @@ -4379,7 +4451,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemShutdownRebootDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemShutdownRebootDescription', { defaultMessage: 'System Shutdown/Reboot (T1529)' } ), id: 'T1529', @@ -4390,7 +4462,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemTimeDiscoveryDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemTimeDiscoveryDescription', { defaultMessage: 'System Time Discovery (T1124)' } ), id: 'T1124', @@ -4401,7 +4473,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.systemdServiceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemdServiceDescription', { defaultMessage: 'Systemd Service (T1501)' } ), id: 'T1501', @@ -4412,7 +4484,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.taintSharedContentDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.taintSharedContentDescription', { defaultMessage: 'Taint Shared Content (T1080)' } ), id: 'T1080', @@ -4423,7 +4495,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.templateInjectionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.templateInjectionDescription', { defaultMessage: 'Template Injection (T1221)' } ), id: 'T1221', @@ -4434,7 +4506,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.thirdPartySoftwareDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.thirdPartySoftwareDescription', { defaultMessage: 'Third-party Software (T1072)' } ), id: 'T1072', @@ -4445,7 +4517,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.timeProvidersDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.timeProvidersDescription', { defaultMessage: 'Time Providers (T1209)' } ), id: 'T1209', @@ -4455,9 +4527,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'timeProviders', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.timestompDescription', { - defaultMessage: 'Timestomp (T1099)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.timestompDescription', + { + defaultMessage: 'Timestomp (T1099)', + } + ), id: 'T1099', name: 'Timestomp', reference: 'https://attack.mitre.org/techniques/T1099', @@ -4466,7 +4541,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.transferDataToCloudAccountDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.transferDataToCloudAccountDescription', { defaultMessage: 'Transfer Data to Cloud Account (T1537)' } ), id: 'T1537', @@ -4477,7 +4552,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.transmittedDataManipulationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.transmittedDataManipulationDescription', { defaultMessage: 'Transmitted Data Manipulation (T1493)' } ), id: 'T1493', @@ -4487,9 +4562,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'transmittedDataManipulation', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.trapDescription', { - defaultMessage: 'Trap (T1154)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trapDescription', + { + defaultMessage: 'Trap (T1154)', + } + ), id: 'T1154', name: 'Trap', reference: 'https://attack.mitre.org/techniques/T1154', @@ -4498,7 +4576,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.trustedDeveloperUtilitiesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedDeveloperUtilitiesDescription', { defaultMessage: 'Trusted Developer Utilities (T1127)' } ), id: 'T1127', @@ -4509,7 +4587,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.trustedRelationshipDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedRelationshipDescription', { defaultMessage: 'Trusted Relationship (T1199)' } ), id: 'T1199', @@ -4520,7 +4598,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.twoFactorAuthenticationInterceptionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.twoFactorAuthenticationInterceptionDescription', { defaultMessage: 'Two-Factor Authentication Interception (T1111)' } ), id: 'T1111', @@ -4531,7 +4609,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.uncommonlyUsedPortDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.uncommonlyUsedPortDescription', { defaultMessage: 'Uncommonly Used Port (T1065)' } ), id: 'T1065', @@ -4542,7 +4620,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.unusedUnsupportedCloudRegionsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.unusedUnsupportedCloudRegionsDescription', { defaultMessage: 'Unused/Unsupported Cloud Regions (T1535)' } ), id: 'T1535', @@ -4553,7 +4631,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.userExecutionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.userExecutionDescription', { defaultMessage: 'User Execution (T1204)' } ), id: 'T1204', @@ -4564,7 +4642,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.validAccountsDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.validAccountsDescription', { defaultMessage: 'Valid Accounts (T1078)' } ), id: 'T1078', @@ -4575,7 +4653,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.videoCaptureDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.videoCaptureDescription', { defaultMessage: 'Video Capture (T1125)' } ), id: 'T1125', @@ -4586,7 +4664,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.virtualizationSandboxEvasionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.virtualizationSandboxEvasionDescription', { defaultMessage: 'Virtualization/Sandbox Evasion (T1497)' } ), id: 'T1497', @@ -4597,7 +4675,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.webServiceDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webServiceDescription', { defaultMessage: 'Web Service (T1102)' } ), id: 'T1102', @@ -4608,7 +4686,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.webSessionCookieDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webSessionCookieDescription', { defaultMessage: 'Web Session Cookie (T1506)' } ), id: 'T1506', @@ -4618,9 +4696,12 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ value: 'webSessionCookie', }, { - label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.webShellDescription', { - defaultMessage: 'Web Shell (T1100)', - }), + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webShellDescription', + { + defaultMessage: 'Web Shell (T1100)', + } + ), id: 'T1100', name: 'Web Shell', reference: 'https://attack.mitre.org/techniques/T1100', @@ -4629,7 +4710,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsAdminSharesDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsAdminSharesDescription', { defaultMessage: 'Windows Admin Shares (T1077)' } ), id: 'T1077', @@ -4640,7 +4721,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationDescription', { defaultMessage: 'Windows Management Instrumentation (T1047)' } ), id: 'T1047', @@ -4651,7 +4732,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationEventSubscriptionDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationEventSubscriptionDescription', { defaultMessage: 'Windows Management Instrumentation Event Subscription (T1084)' } ), id: 'T1084', @@ -4662,7 +4743,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsRemoteManagementDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsRemoteManagementDescription', { defaultMessage: 'Windows Remote Management (T1028)' } ), id: 'T1028', @@ -4673,7 +4754,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.winlogonHelperDllDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.winlogonHelperDllDescription', { defaultMessage: 'Winlogon Helper DLL (T1004)' } ), id: 'T1004', @@ -4684,7 +4765,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.siem.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription', + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription', { defaultMessage: 'XSL Script Processing (T1220)' } ), id: 'T1220', diff --git a/x-pack/plugins/siem/public/alerts/mitre/types.ts b/x-pack/plugins/security_solution/public/alerts/mitre/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/mitre/types.ts rename to x-pack/plugins/security_solution/public/alerts/mitre/types.ts diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx similarity index 92% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx index 9632ddfeadc0d..0c58f5620964b 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx @@ -9,12 +9,13 @@ import React from 'react'; import { useKibana } from '../../../common/lib/kibana'; import { EmptyPage } from '../../../common/components/empty_page'; import * as i18n from '../../../common/translations'; +import { ADD_DATA_PATH } from '../../../../common/constants'; export const DetectionEngineEmptyPage = React.memo(() => ( + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.create.successfullyCreatedRuleTitle', + { + values: { ruleName }, + defaultMessage: '{ruleName} was created', + } + ); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx new file mode 100644 index 0000000000000..ebd6ed118f937 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -0,0 +1,429 @@ +/* + * 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. + */ + +/* eslint-disable react-hooks/rules-of-hooks */ + +import { + EuiButton, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTab, + EuiTabs, + EuiToolTip, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, memo, useCallback, useMemo, useState } from 'react'; +import { Redirect, useParams } from 'react-router-dom'; +import { StickyContainer } from 'react-sticky'; +import { connect, ConnectedProps } from 'react-redux'; + +import { UpdateDateRange } from '../../../../../common/components/charts/common'; +import { FiltersGlobal } from '../../../../../common/components/filters_global'; +import { FormattedDate } from '../../../../../common/components/formatted_date'; +import { + getEditRuleUrl, + getRulesUrl, + DETECTION_ENGINE_PAGE_NAME, +} from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { SiemSearchBar } from '../../../../../common/components/search_bar'; +import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { useRule } from '../../../../../alerts/containers/detection_engine/rules'; + +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../../../../common/containers/source'; +import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; + +import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; +import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; +import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; +import { AlertsTable } from '../../../../components/alerts_table'; +import { useUserInfo } from '../../../../components/user_info'; +import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; +import { useAlertInfo } from '../../../../components/alerts_info'; +import { StepDefineRule } from '../../../../components/rules/step_define_rule'; +import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; +import { buildAlertsRuleIdFilter } from '../../../../components/alerts_table/default_config'; +import { NoWriteAlertsCallOut } from '../../../../components/no_write_alerts_callout'; +import * as detectionI18n from '../../translations'; +import { ReadOnlyCallOut } from '../../../../components/rules/read_only_callout'; +import { RuleSwitch } from '../../../../components/rules/rule_switch'; +import { StepPanel } from '../../../../components/rules/step_panel'; +import { getStepsData, redirectToDetections, userHasNoPermissions } from '../helpers'; +import * as ruleI18n from '../translations'; +import * as i18n from './translations'; +import { GlobalTime } from '../../../../../common/containers/global_time'; +import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; +import { inputsSelectors } from '../../../../../common/store/inputs'; +import { State } from '../../../../../common/store'; +import { InputsRange } from '../../../../../common/store/inputs/model'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; +import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_overflow'; +import { RuleStatusFailedCallOut } from './status_failed_callout'; +import { FailureHistory } from './failure_history'; +import { RuleStatus } from '../../../../components/rules//rule_status'; +import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; + +enum RuleDetailTabs { + alerts = 'alerts', + failures = 'failures', +} + +const ruleDetailTabs = [ + { + id: RuleDetailTabs.alerts, + name: detectionI18n.ALERT, + disabled: false, + }, + { + id: RuleDetailTabs.failures, + name: i18n.FAILURE_HISTORY_TAB, + disabled: false, + }, +]; + +export const RuleDetailsPageComponent: FC = ({ + filters, + query, + setAbsoluteRangeDatePicker, +}) => { + const { + loading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + signalIndexName, + } = useUserInfo(); + const { detailName: ruleId } = useParams(); + const [isLoading, rule] = useRule(ruleId); + // This is used to re-trigger api rule status when user de/activate rule + const [ruleEnabled, setRuleEnabled] = useState(null); + const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); + const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = + rule != null + ? getStepsData({ rule, detailsView: true }) + : { + aboutRuleData: null, + modifiedAboutRuleDetailsData: null, + defineRuleData: null, + scheduleRuleData: null, + }; + const [lastAlerts] = useAlertInfo({ ruleId }); + const mlCapabilities = useMlCapabilities(); + + // TODO: Refactor license check + hasMlAdminPermissions to common check + const hasMlPermissions = + mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + + const title = isLoading === true || rule === null ? : rule.name; + const subTitle = useMemo( + () => + isLoading === true || rule === null ? ( + + ) : ( + [ + + ), + }} + />, + rule?.updated_by != null ? ( + + ), + }} + /> + ) : ( + '' + ), + ] + ), + [isLoading, rule] + ); + + const alertDefaultFilters = useMemo( + () => (ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), + [ruleId] + ); + + const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ + alertDefaultFilters, + filters, + ]); + + const tabs = useMemo( + () => ( + + {ruleDetailTabs.map((tab) => ( + setRuleDetailTab(tab.id)} + isSelected={tab.id === ruleDetailTab} + disabled={tab.disabled} + key={tab.id} + > + {tab.name} + + ))} + + ), + [ruleDetailTabs, ruleDetailTab, setRuleDetailTab] + ); + const ruleError = useMemo( + () => + rule?.status === 'failed' && + ruleDetailTab === RuleDetailTabs.alerts && + rule?.last_failure_at != null ? ( + + ) : null, + [rule, ruleDetailTab] + ); + + const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ + signalIndexName, + ]); + + const updateDateRangeCallback = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + const handleOnChangeEnabledRule = useCallback( + (enabled: boolean) => { + if (ruleEnabled == null || enabled !== ruleEnabled) { + setRuleEnabled(enabled); + } + }, + [ruleEnabled, setRuleEnabled] + ); + + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; + } + + return ( + <> + {hasIndexWrite != null && !hasIndexWrite && } + {userHasNoPermissions(canUserCRUD) && } + + {({ indicesExist, indexPattern }) => { + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + {({ to, from, deleteQuery, setQuery }) => ( + + + + + + + + {detectionI18n.LAST_ALERT} + {': '} + {lastAlerts} + , + ] + : []), + , + ]} + title={title} + > + + + + + + + + + + + + {ruleI18n.EDIT_RULE_SETTINGS} + + + + + + + + + + {ruleError} + + + + + + + + + + + {defineRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + + + + + {tabs} + + {ruleDetailTab === RuleDetailTabs.alerts && ( + <> + + + {ruleId != null && ( + + )} + + )} + {ruleDetailTab === RuleDetailTabs.failures && } + + + )} + + ) : ( + + + + + + ); + }} + + + + + ); +}; + +const makeMapStateToProps = () => { + const getGlobalInputs = inputsSelectors.globalSelector(); + return (state: State) => { + const globalInputs: InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + + return { + query, + filters, + }; + }; +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const RuleDetailsPage = connector(memo(RuleDetailsPageComponent)); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts new file mode 100644 index 0000000000000..9cf510f4a9b5d --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.pageTitle', + { + defaultMessage: 'Rule details', + } +); + +export const BACK_TO_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.backToRulesDescription', + { + defaultMessage: 'Back to detection rules', + } +); + +export const EXPERIMENTAL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription', + { + defaultMessage: 'Experimental', + } +); + +export const ACTIVATE_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.activateRuleLabel', + { + defaultMessage: 'Activate', + } +); + +export const UNKNOWN = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.unknownDescription', + { + defaultMessage: 'Unknown', + } +); + +export const ERROR_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle', + { + defaultMessage: 'Rule failure at', + } +); + +export const FAILURE_HISTORY_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.failureHistoryTab', + { + defaultMessage: 'Failure History', + } +); + +export const LAST_FIVE_ERRORS = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.lastFiveErrorsTitle', + { + defaultMessage: 'Last five errors', + } +); + +export const COLUMN_STATUS_TYPE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.statusTypeColumn', + { + defaultMessage: 'Type', + } +); + +export const COLUMN_FAILED_AT = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.statusFailedAtColumn', + { + defaultMessage: 'Failed at', + } +); + +export const COLUMN_FAILED_MSG = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.statusFailedMsgColumn', + { + defaultMessage: 'Failed message', + } +); + +export const TYPE_FAILED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.statusFailedDescription', + { + defaultMessage: 'Failed', + } +); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx new file mode 100644 index 0000000000000..8fc646c01b17c --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx @@ -0,0 +1,431 @@ +/* + * 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. + */ + +/* eslint-disable react-hooks/rules-of-hooks */ + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Redirect, useParams } from 'react-router-dom'; + +import { useRule, usePersistRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; +import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; +import { useUserInfo } from '../../../../components/user_info'; +import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; +import { FormHook, FormData } from '../../../../../shared_imports'; +import { StepPanel } from '../../../../components/rules/step_panel'; +import { StepAboutRule } from '../../../../components/rules/step_about_rule'; +import { StepDefineRule } from '../../../../components/rules/step_define_rule'; +import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; +import { StepRuleActions } from '../../../../components/rules/step_rule_actions'; +import { formatRule } from '../create/helpers'; +import { + getStepsData, + redirectToDetections, + getActionMessageParams, + userHasNoPermissions, +} from '../helpers'; +import * as ruleI18n from '../translations'; +import { + RuleStep, + DefineStepRule, + AboutStepRule, + ScheduleStepRule, + ActionsStepRule, +} from '../types'; +import * as i18n from './translations'; + +interface StepRuleForm { + isValid: boolean; +} +interface AboutStepRuleForm extends StepRuleForm { + data: AboutStepRule | null; +} +interface DefineStepRuleForm extends StepRuleForm { + data: DefineStepRule | null; +} +interface ScheduleStepRuleForm extends StepRuleForm { + data: ScheduleStepRule | null; +} + +interface ActionsStepRuleForm extends StepRuleForm { + data: ActionsStepRule | null; +} + +const EditRulePageComponent: FC = () => { + const [, dispatchToaster] = useStateToaster(); + const { + loading: initLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + } = useUserInfo(); + const { detailName: ruleId } = useParams(); + const [loading, rule] = useRule(ruleId); + + const [initForm, setInitForm] = useState(false); + const [myAboutRuleForm, setMyAboutRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myDefineRuleForm, setMyDefineRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myScheduleRuleForm, setMyScheduleRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myActionsRuleForm, setMyActionsRuleForm] = useState({ + data: null, + isValid: false, + }); + const [selectedTab, setSelectedTab] = useState(); + const stepsForm = useRef | null>>({ + [RuleStep.defineRule]: null, + [RuleStep.aboutRule]: null, + [RuleStep.scheduleRule]: null, + [RuleStep.ruleActions]: null, + }); + const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const [tabHasError, setTabHasError] = useState([]); + const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]); + const setStepsForm = useCallback( + (step: RuleStep, form: FormHook) => { + stepsForm.current[step] = form; + if (initForm && step === (selectedTab?.id as RuleStep) && form.isSubmitted === false) { + setInitForm(false); + form.submit(); + } + }, + [initForm, selectedTab] + ); + const tabs = useMemo( + () => [ + { + id: RuleStep.defineRule, + name: ruleI18n.DEFINITION, + disabled: rule?.immutable, + content: ( + <> + + + {myDefineRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.aboutRule, + name: ruleI18n.ABOUT, + disabled: rule?.immutable, + content: ( + <> + + + {myAboutRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.scheduleRule, + name: ruleI18n.SCHEDULE, + disabled: rule?.immutable, + content: ( + <> + + + {myScheduleRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.ruleActions, + name: ruleI18n.ACTIONS, + content: ( + <> + + + {myActionsRuleForm.data != null && ( + + )} + + + + ), + }, + ], + [ + rule, + loading, + initLoading, + isLoading, + myAboutRuleForm, + myDefineRuleForm, + myScheduleRuleForm, + myActionsRuleForm, + setStepsForm, + stepsForm, + actionMessageParams, + ] + ); + + const onSubmit = useCallback(async () => { + const activeFormId = selectedTab?.id as RuleStep; + const activeForm = await stepsForm.current[activeFormId]?.submit(); + + const invalidForms = [ + RuleStep.aboutRule, + RuleStep.defineRule, + RuleStep.scheduleRule, + RuleStep.ruleActions, + ].reduce((acc, step) => { + if ( + (step === activeFormId && activeForm != null && !activeForm?.isValid) || + (step === RuleStep.aboutRule && !myAboutRuleForm.isValid) || + (step === RuleStep.defineRule && !myDefineRuleForm.isValid) || + (step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid) || + (step === RuleStep.ruleActions && !myActionsRuleForm.isValid) + ) { + return [...acc, step]; + } + return acc; + }, []); + + if (invalidForms.length === 0 && activeForm != null) { + setTabHasError([]); + setRule({ + ...formatRule( + (activeFormId === RuleStep.defineRule + ? activeForm.data + : myDefineRuleForm.data) as DefineStepRule, + (activeFormId === RuleStep.aboutRule + ? activeForm.data + : myAboutRuleForm.data) as AboutStepRule, + (activeFormId === RuleStep.scheduleRule + ? activeForm.data + : myScheduleRuleForm.data) as ScheduleStepRule, + (activeFormId === RuleStep.ruleActions + ? activeForm.data + : myActionsRuleForm.data) as ActionsStepRule + ), + ...(ruleId ? { id: ruleId } : {}), + }); + } else { + setTabHasError(invalidForms); + } + }, [ + stepsForm, + myAboutRuleForm, + myDefineRuleForm, + myScheduleRuleForm, + myActionsRuleForm, + selectedTab, + ruleId, + ]); + + useEffect(() => { + if (rule != null) { + const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ + rule, + }); + setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); + setMyDefineRuleForm({ data: defineRuleData, isValid: true }); + setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); + setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); + } + }, [rule]); + + const onTabClick = useCallback( + async (tab: EuiTabbedContentTab) => { + if (selectedTab != null) { + const ruleStep = selectedTab.id as RuleStep; + const respForm = await stepsForm.current[ruleStep]?.submit(); + + if (respForm != null) { + if (ruleStep === RuleStep.aboutRule) { + setMyAboutRuleForm({ + data: respForm.data as AboutStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.defineRule) { + setMyDefineRuleForm({ + data: respForm.data as DefineStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.scheduleRule) { + setMyScheduleRuleForm({ + data: respForm.data as ScheduleStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.ruleActions) { + setMyActionsRuleForm({ + data: respForm.data as ActionsStepRule, + isValid: respForm.isValid, + }); + } + } + } + setInitForm(true); + setSelectedTab(tab); + }, + [selectedTab, stepsForm.current] + ); + + useEffect(() => { + if (rule != null) { + const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ + rule, + }); + setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); + setMyDefineRuleForm({ data: defineRuleData, isValid: true }); + setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); + setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); + } + }, [rule]); + + useEffect(() => { + const tabIndex = rule?.immutable ? 3 : 0; + setSelectedTab(tabs[tabIndex]); + }, [rule]); + + if (isSaved) { + displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); + return ; + } + + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; + } else if (userHasNoPermissions(canUserCRUD)) { + return ; + } + + return ( + <> + + + {tabHasError.length > 0 && ( + + { + if (t === RuleStep.aboutRule) { + return ruleI18n.ABOUT; + } else if (t === RuleStep.defineRule) { + return ruleI18n.DEFINITION; + } else if (t === RuleStep.scheduleRule) { + return ruleI18n.SCHEDULE; + } else if (t === RuleStep.ruleActions) { + return ruleI18n.RULE_ACTIONS; + } + return t; + }) + .join(', '), + }} + /> + + )} + + t.id === selectedTab?.id)} + onTabClick={onTabClick} + tabs={tabs} + /> + + + + + + + {i18n.CANCEL} + + + + + + {i18n.SAVE_CHANGES} + + + + + + + + ); +}; + +export const EditRulePage = memo(EditRulePageComponent); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/translations.ts new file mode 100644 index 0000000000000..dcd55caf6549e --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/translations.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.editRule.pageTitle', + { + defaultMessage: 'Edit rule settings', + } +); + +export const CANCEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.editRule.cancelTitle', + { + defaultMessage: 'Cancel', + } +); + +export const SAVE_CHANGES = i18n.translate( + 'xpack.securitySolution.detectionEngine.editRule.saveChangeTitle', + { + defaultMessage: 'Save changes', + } +); + +export const SORRY_ERRORS = i18n.translate( + 'xpack.securitySolution.detectionEngine.editRule.errorMsgDescription', + { + defaultMessage: 'Sorry', + } +); + +export const BACK_TO = i18n.translate( + 'xpack.securitySolution.detectionEngine.editRule.backToDescription', + { + defaultMessage: 'Back to', + } +); + +export const SUCCESSFULLY_SAVED_RULE = (ruleName: string) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.update.successfullySavedRuleTitle', { + values: { ruleName }, + defaultMessage: '{ruleName} was saved', + }); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.test.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.tsx rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts new file mode 100644 index 0000000000000..32f59093e6a04 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts @@ -0,0 +1,530 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const BACK_TO_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.backOptionsHeader', + { + defaultMessage: 'Back to alerts', + } +); + +export const IMPORT_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.importRuleTitle', + { + defaultMessage: 'Import rule…', + } +); + +export const ADD_NEW_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addNewRuleTitle', + { + defaultMessage: 'Create new rule', + } +); + +export const PAGE_TITLE = i18n.translate('xpack.securitySolution.detectionEngine.rules.pageTitle', { + defaultMessage: 'Detection rules', +}); + +export const ADD_PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.addPageTitle', + { + defaultMessage: 'Create', + } +); + +export const EDIT_PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.editPageTitle', + { + defaultMessage: 'Edit', + } +); + +export const REFRESH = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.refreshTitle', + { + defaultMessage: 'Refresh', + } +); + +export const BATCH_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle', + { + defaultMessage: 'Bulk actions', + } +); + +export const ACTIVE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription', + { + defaultMessage: 'active', + } +); + +export const INACTIVE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.inactiveRuleDescription', + { + defaultMessage: 'inactive', + } +); + +export const BATCH_ACTION_ACTIVATE_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedTitle', + { + defaultMessage: 'Activate selected', + } +); + +export const BATCH_ACTION_ACTIVATE_SELECTED_ERROR = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle', + { + values: { totalRules }, + defaultMessage: 'Error activating {totalRules, plural, =1 {rule} other {rules}}…', + } + ); + +export const BATCH_ACTION_DEACTIVATE_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle', + { + defaultMessage: 'Deactivate selected', + } +); + +export const BATCH_ACTION_DEACTIVATE_SELECTED_ERROR = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle', + { + values: { totalRules }, + defaultMessage: 'Error deactivating {totalRules, plural, =1 {rule} other {rules}}…', + } + ); + +export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.exportSelectedTitle', + { + defaultMessage: 'Export selected', + } +); + +export const BATCH_ACTION_DUPLICATE_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle', + { + defaultMessage: 'Duplicate selected…', + } +); + +export const BATCH_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected…', + } +); + +export const BATCH_ACTION_DELETE_SELECTED_IMMUTABLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle', + { + defaultMessage: 'Selection contains immutable rules which cannot be deleted', + } +); + +export const BATCH_ACTION_DELETE_SELECTED_ERROR = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle', + { + values: { totalRules }, + defaultMessage: 'Error deleting {totalRules, plural, =1 {rule} other {rules}}…', + } + ); + +export const EXPORT_FILENAME = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle', + { + defaultMessage: 'rules_export', + } +); + +export const SUCCESSFULLY_EXPORTED_RULES = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedRulesTitle', + { + values: { totalRules }, + defaultMessage: + 'Successfully exported {totalRules, plural, =0 {all rules} =1 {{totalRules} rule} other {{totalRules} rules}}', + } + ); + +export const ALL_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.tableTitle', + { + defaultMessage: 'All rules', + } +); + +export const SEARCH_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.searchAriaLabel', + { + defaultMessage: 'Search rules', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.searchPlaceholder', + { + defaultMessage: 'e.g. rule name', + } +); + +export const SHOWING_RULES = (totalRules: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const SELECTED_RULES = (selectedRules: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle', { + values: { selectedRules }, + defaultMessage: 'Selected {selectedRules} {selectedRules, plural, =1 {rule} other {rules}}', + }); + +export const EDIT_RULE_SETTINGS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription', + { + defaultMessage: 'Edit rule settings', + } +); + +export const DUPLICATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateTitle', + { + defaultMessage: 'Duplicate', + } +); + +export const DUPLICATE_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription', + { + defaultMessage: 'Duplicate rule…', + } +); + +export const SUCCESSFULLY_DUPLICATED_RULES = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.successfullyDuplicatedRulesTitle', + { + values: { totalRules }, + defaultMessage: + 'Successfully duplicated {totalRules, plural, =1 {{totalRules} rule} other {{totalRules} rules}}', + } + ); + +export const DUPLICATE_RULE_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription', + { + defaultMessage: 'Error duplicating rule…', + } +); + +export const EXPORT_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription', + { + defaultMessage: 'Export rule', + } +); + +export const DELETE_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription', + { + defaultMessage: 'Delete rule…', + } +); + +export const COLUMN_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const COLUMN_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.riskScoreTitle', + { + defaultMessage: 'Risk score', + } +); + +export const COLUMN_SEVERITY = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const COLUMN_LAST_COMPLETE_RUN = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.lastRunTitle', + { + defaultMessage: 'Last run', + } +); + +export const COLUMN_LAST_RESPONSE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.lastResponseTitle', + { + defaultMessage: 'Last response', + } +); + +export const COLUMN_TAGS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle', + { + defaultMessage: 'Tags', + } +); + +export const COLUMN_ACTIVATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.activateTitle', + { + defaultMessage: 'Activated', + } +); + +export const COLUMN_INDEXING_TIMES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.indexingTimes', + { + defaultMessage: 'Indexing Time (ms)', + } +); + +export const COLUMN_QUERY_TIMES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.queryTimes', + { + defaultMessage: 'Query Time (ms)', + } +); + +export const COLUMN_GAP = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.gap', + { + defaultMessage: 'Gap (if any)', + } +); + +export const COLUMN_LAST_LOOKBACK_DATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.lastLookBackDate', + { + defaultMessage: 'Last Look-Back Date', + } +); + +export const RULES_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.tabs.rules', + { + defaultMessage: 'Rules', + } +); + +export const MONITORING_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring', + { + defaultMessage: 'Monitoring', + } +); + +export const CUSTOM_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle', + { + defaultMessage: 'Custom rules', + } +); + +export const ELASTIC_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle', + { + defaultMessage: 'Elastic rules', + } +); + +export const TAGS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.filters.tagsLabel', + { + defaultMessage: 'Tags', + } +); + +export const NO_TAGS_AVAILABLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.filters.noTagsAvailableDescription', + { + defaultMessage: 'No tags available', + } +); + +export const NO_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesTitle', + { + defaultMessage: 'No rules found', + } +); + +export const NO_RULES_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesBodyTitle', + { + defaultMessage: "We weren't able to find any rules with the above filters.", + } +); + +export const DEFINE_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.defineRuleTitle', + { + defaultMessage: 'Define rule', + } +); + +export const ABOUT_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.aboutRuleTitle', + { + defaultMessage: 'About rule', + } +); + +export const SCHEDULE_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.scheduleRuleTitle', + { + defaultMessage: 'Schedule rule', + } +); + +export const RULE_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.ruleActionsTitle', + { + defaultMessage: 'Rule actions', + } +); + +export const DEFINITION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.stepDefinitionTitle', + { + defaultMessage: 'Definition', + } +); + +export const ABOUT = i18n.translate('xpack.securitySolution.detectionEngine.rules.stepAboutTitle', { + defaultMessage: 'About', +}); + +export const SCHEDULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.stepScheduleTitle', + { + defaultMessage: 'Schedule', + } +); + +export const ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.stepActionsTitle', + { + defaultMessage: 'Actions', + } +); + +export const OPTIONAL_FIELD = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.optionalFieldDescription', + { + defaultMessage: 'Optional', + } +); + +export const CONTINUE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.continueButtonTitle', + { + defaultMessage: 'Continue', + } +); + +export const UPDATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.updateButtonTitle', + { + defaultMessage: 'Update', + } +); + +export const DELETE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.deleteDescription', + { + defaultMessage: 'Delete', + } +); + +export const LOAD_PREPACKAGED_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton', + { + defaultMessage: 'Load Elastic prebuilt rules', + } +); + +export const RELOAD_MISSING_PREPACKAGED_RULES = (missingRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton', + { + values: { missingRules }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', + } + ); + +export const IMPORT_RULE_BTN_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle', + { + defaultMessage: 'Import rule', + } +); + +export const SELECT_RULE = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription', + { + defaultMessage: 'Select a SIEM rule (as exported from the Detection Engine view) to import', + } +); + +export const INITIAL_PROMPT_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.importRuleModal.initialPromptTextDescription', + { + defaultMessage: 'Select or drag and drop a valid rules_export.ndjson file', + } +); + +export const OVERWRITE_WITH_SAME_NAME = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.importRuleModal.overwriteDescription', + { + defaultMessage: 'Automatically overwrite saved objects with the same rule ID', + } +); + +export const SUCCESSFULLY_IMPORTED_RULES = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle', + { + values: { totalRules }, + defaultMessage: + 'Successfully imported {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + } + ); + +export const IMPORT_FAILED = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedTitle', + { + defaultMessage: 'Failed to import rules', + } +); + +export const IMPORT_FAILED_DETAILED = (ruleId: string, statusCode: number, message: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedDetailedTitle', + { + values: { ruleId, statusCode, message }, + defaultMessage: 'Rule ID: {ruleId}\n Status Code: {statusCode}\n Message: {message}', + } + ); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts rename to x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts new file mode 100644 index 0000000000000..a19422d0de2c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.detectionsPageTitle', + { + defaultMessage: 'Alerts', + } +); + +export const LAST_ALERT = i18n.translate('xpack.securitySolution.detectionEngine.lastSignalTitle', { + defaultMessage: 'Last alert', +}); + +export const TOTAL_SIGNAL = i18n.translate( + 'xpack.securitySolution.detectionEngine.totalSignalTitle', + { + defaultMessage: 'Total', + } +); + +export const SIGNAL = i18n.translate('xpack.securitySolution.detectionEngine.signalTitle', { + defaultMessage: 'Detected alerts', +}); + +export const ALERT = i18n.translate('xpack.securitySolution.detectionEngine.alertTitle', { + defaultMessage: 'External alerts', +}); + +export const BUTTON_MANAGE_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.buttonManageRules', + { + defaultMessage: 'Manage detection rules', + } +); + +export const PANEL_SUBTITLE_SHOWING = i18n.translate( + 'xpack.securitySolution.detectionEngine.panelSubtitleShowing', + { + defaultMessage: 'Showing', + } +); + +export const EMPTY_TITLE = i18n.translate('xpack.securitySolution.detectionEngine.emptyTitle', { + defaultMessage: + 'It looks like you don’t have any indices relevant to the detection engine in the SIEM application', +}); + +export const EMPTY_ACTION_PRIMARY = i18n.translate( + 'xpack.securitySolution.detectionEngine.emptyActionPrimary', + { + defaultMessage: 'View setup instructions', + } +); + +export const EMPTY_ACTION_SECONDARY = i18n.translate( + 'xpack.securitySolution.detectionEngine.emptyActionSecondary', + { + defaultMessage: 'Go to documentation', + } +); + +export const NO_INDEX_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.noIndexTitle', + { + defaultMessage: 'Let’s set up your detection engine', + } +); + +export const NO_INDEX_MSG_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.noIndexMsgBody', + { + defaultMessage: + 'To use the detection engine, a user with the required cluster and index privileges must first access this page. For more help, contact your administrator.', + } +); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.securitySolution.detectionEngine.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const USER_UNAUTHENTICATED_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.userUnauthenticatedTitle', + { + defaultMessage: 'Detection engine permissions required', + } +); + +export const USER_UNAUTHENTICATED_MSG_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.userUnauthenticatedMsgBody', + { + defaultMessage: + 'You do not have the required permissions for viewing the detection engine. For more help, contact your administrator.', + } +); + +export const ML_RULES_DISABLED_MESSAGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.mlRulesDisabledMessageTitle', + { + defaultMessage: 'ML rules require Platinum License and ML Admin Permissions', + } +); + +export const ML_RULES_UNAVAILABLE = (totalRules: number) => + i18n.translate('xpack.securitySolution.detectionEngine.mlUnavailableTitle', { + values: { totalRules }, + defaultMessage: + '{totalRules} {totalRules, plural, =1 {rule requires} other {rules require}} Machine Learning to enable.', + }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/types.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/types.ts new file mode 100644 index 0000000000000..d529d99ad3ad4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum DetectionEngineTab { + signals = 'signals', + alerts = 'alerts', +} diff --git a/x-pack/plugins/siem/public/alerts/routes.tsx b/x-pack/plugins/security_solution/public/alerts/routes.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/routes.tsx rename to x-pack/plugins/security_solution/public/alerts/routes.tsx diff --git a/x-pack/plugins/siem/public/app/404.tsx b/x-pack/plugins/security_solution/public/app/404.tsx similarity index 88% rename from x-pack/plugins/siem/public/app/404.tsx rename to x-pack/plugins/security_solution/public/app/404.tsx index 6a1b5c56dc853..b24cad5ad5b4b 100644 --- a/x-pack/plugins/siem/public/app/404.tsx +++ b/x-pack/plugins/security_solution/public/app/404.tsx @@ -12,7 +12,7 @@ import { WrapperPage } from '../common/components/wrapper_page'; export const NotFoundPage = React.memo(() => ( diff --git a/x-pack/plugins/siem/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx similarity index 84% rename from x-pack/plugins/siem/public/app/app.tsx rename to x-pack/plugins/security_solution/public/app/app.tsx index 732b1883b9b77..50a24ef012b8b 100644 --- a/x-pack/plugins/siem/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -29,6 +29,7 @@ import { PageRouter } from './routes'; import { createStore, createInitialState } from '../common/store'; import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters'; import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider'; +import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { ApolloClientContext } from '../common/utils/apollo_context'; import { SecuritySubPlugins } from './types'; @@ -49,19 +50,21 @@ const AppPluginRootComponent: React.FC = ({ history, }) => ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/siem/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx similarity index 100% rename from x-pack/plugins/siem/public/app/home/home_navigations.tsx rename to x-pack/plugins/security_solution/public/app/home/home_navigations.tsx diff --git a/x-pack/plugins/siem/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/app/home/index.tsx rename to x-pack/plugins/security_solution/public/app/home/index.tsx diff --git a/x-pack/plugins/siem/public/app/home/setup.tsx b/x-pack/plugins/security_solution/public/app/home/setup.tsx similarity index 90% rename from x-pack/plugins/siem/public/app/home/setup.tsx rename to x-pack/plugins/security_solution/public/app/home/setup.tsx index a88b6a36100db..5b977a83302a9 100644 --- a/x-pack/plugins/siem/public/app/home/setup.tsx +++ b/x-pack/plugins/security_solution/public/app/home/setup.tsx @@ -13,11 +13,11 @@ export const Setup: React.FunctionComponent<{ notifications: NotificationsStart; }> = ({ ingestManager, notifications }) => { React.useEffect(() => { - const defaultText = i18n.translate('xpack.siem.endpoint.ingestToastMessage', { + const defaultText = i18n.translate('xpack.securitySolution.endpoint.ingestToastMessage', { defaultMessage: 'Ingest Manager failed during its setup.', }); - const title = i18n.translate('xpack.siem.endpoint.ingestToastTitle', { + const title = i18n.translate('xpack.securitySolution.endpoint.ingestToastTitle', { defaultMessage: 'App failed to initialize', }); diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/home/translations.ts new file mode 100644 index 0000000000000..ccf927eba20c9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/home/translations.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const OVERVIEW = i18n.translate('xpack.securitySolution.navigation.overview', { + defaultMessage: 'Overview', +}); + +export const HOSTS = i18n.translate('xpack.securitySolution.navigation.hosts', { + defaultMessage: 'Hosts', +}); + +export const NETWORK = i18n.translate('xpack.securitySolution.navigation.network', { + defaultMessage: 'Network', +}); + +export const DETECTION_ENGINE = i18n.translate( + 'xpack.securitySolution.navigation.detectionEngine', + { + defaultMessage: 'Detections', + } +); + +export const TIMELINES = i18n.translate('xpack.securitySolution.navigation.timelines', { + defaultMessage: 'Timelines', +}); + +export const CASE = i18n.translate('xpack.securitySolution.navigation.case', { + defaultMessage: 'Cases', +}); + +export const MANAGEMENT = i18n.translate('xpack.securitySolution.navigation.management', { + defaultMessage: 'Management', +}); diff --git a/x-pack/plugins/siem/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/app/index.tsx rename to x-pack/plugins/security_solution/public/app/index.tsx diff --git a/x-pack/plugins/siem/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx similarity index 100% rename from x-pack/plugins/siem/public/app/routes.tsx rename to x-pack/plugins/security_solution/public/app/routes.tsx diff --git a/x-pack/plugins/siem/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts similarity index 100% rename from x-pack/plugins/siem/public/app/types.ts rename to x-pack/plugins/security_solution/public/app/types.ts diff --git a/x-pack/plugins/siem/public/cases/components/__mock__/form.ts b/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/components/__mock__/form.ts rename to x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts diff --git a/x-pack/plugins/siem/public/cases/components/__mock__/router.ts b/x-pack/plugins/security_solution/public/cases/components/__mock__/router.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/components/__mock__/router.ts rename to x-pack/plugins/security_solution/public/cases/components/__mock__/router.ts diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/add_comment/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/add_comment/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/add_comment/schema.tsx rename to x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/translations.ts b/x-pack/plugins/security_solution/public/cases/components/add_comment/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/components/add_comment/translations.ts rename to x-pack/plugins/security_solution/public/cases/components/add_comment/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/all_cases/actions.tsx rename to x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/columns.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/all_cases/columns.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/all_cases/columns.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/all_cases/columns.tsx rename to x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/all_cases/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/all_cases/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/table_filters.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/all_cases/table_filters.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/table_filters.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/all_cases/table_filters.tsx rename to x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/translations.ts b/x-pack/plugins/security_solution/public/cases/components/all_cases/translations.ts new file mode 100644 index 0000000000000..9198aaceb96f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/translations.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const NO_CASES = i18n.translate('xpack.securitySolution.case.caseTable.noCases.title', { + defaultMessage: 'No Cases', +}); +export const NO_CASES_BODY = i18n.translate('xpack.securitySolution.case.caseTable.noCases.body', { + defaultMessage: + 'There are no cases to display. Please create a new case or change your filter settings above.', +}); + +export const ADD_NEW_CASE = i18n.translate('xpack.securitySolution.case.caseTable.addNewCase', { + defaultMessage: 'Add New Case', +}); + +export const SHOWING_SELECTED_CASES = (totalRules: number) => + i18n.translate('xpack.securitySolution.case.caseTable.selectedCasesTitle', { + values: { totalRules }, + defaultMessage: 'Selected {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + +export const SHOWING_CASES = (totalRules: number) => + i18n.translate('xpack.securitySolution.case.caseTable.showingCasesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.case.caseTable.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, + }); + +export const SEARCH_CASES = i18n.translate( + 'xpack.securitySolution.case.caseTable.searchAriaLabel', + { + defaultMessage: 'Search cases', + } +); + +export const BULK_ACTIONS = i18n.translate('xpack.securitySolution.case.caseTable.bulkActions', { + defaultMessage: 'Bulk actions', +}); + +export const EXTERNAL_INCIDENT = i18n.translate( + 'xpack.securitySolution.case.caseTable.snIncident', + { + defaultMessage: 'External Incident', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM = i18n.translate( + 'xpack.securitySolution.case.caseTable.incidentSystem', + { + defaultMessage: 'Incident Management System', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.case.caseTable.searchPlaceholder', + { + defaultMessage: 'e.g. case name', + } +); +export const OPEN_CASES = i18n.translate('xpack.securitySolution.case.caseTable.openCases', { + defaultMessage: 'Open cases', +}); +export const CLOSED_CASES = i18n.translate('xpack.securitySolution.case.caseTable.closedCases', { + defaultMessage: 'Closed cases', +}); + +export const CLOSED = i18n.translate('xpack.securitySolution.case.caseTable.closed', { + defaultMessage: 'Closed', +}); + +export const DELETE = i18n.translate('xpack.securitySolution.case.caseTable.delete', { + defaultMessage: 'Delete', +}); + +export const REQUIRES_UPDATE = i18n.translate( + 'xpack.securitySolution.case.caseTable.requiresUpdate', + { + defaultMessage: ' requires update', + } +); + +export const UP_TO_DATE = i18n.translate('xpack.securitySolution.case.caseTable.upToDate', { + defaultMessage: ' is up to date', +}); +export const NOT_PUSHED = i18n.translate('xpack.securitySolution.case.caseTable.notPushed', { + defaultMessage: 'Not pushed', +}); + +export const REFRESH = i18n.translate('xpack.securitySolution.case.caseTable.refreshTitle', { + defaultMessage: 'Refresh', +}); + +export const SERVICENOW_LINK_ARIA = i18n.translate( + 'xpack.securitySolution.case.caseTable.serviceNowLinkAria', + { + defaultMessage: 'click to view the incident on servicenow', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/bulk_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/bulk_actions/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/translations.ts b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/translations.ts new file mode 100644 index 0000000000000..25e22aebb6a0b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const BULK_ACTION_CLOSE_SELECTED = i18n.translate( + 'xpack.securitySolution.case.caseTable.bulkActions.closeSelectedTitle', + { + defaultMessage: 'Close selected', + } +); + +export const BULK_ACTION_OPEN_SELECTED = i18n.translate( + 'xpack.securitySolution.case.caseTable.bulkActions.openSelectedTitle', + { + defaultMessage: 'Reopen selected', + } +); + +export const BULK_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.securitySolution.case.caseTable.bulkActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/callout/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/callout/helpers.tsx rename to x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/callout/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/callout/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/callout/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/callout/index.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts new file mode 100644 index 0000000000000..01956ca942997 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate( + 'xpack.securitySolution.case.readOnlySavedObjectTitle', + { + defaultMessage: 'You have read-only feature privileges', + } +); + +export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( + 'xpack.securitySolution.case.readOnlySavedObjectDescription', + { + defaultMessage: + 'You are only allowed to view cases. If you need to open and update cases, contact your Kibana administrator', + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.securitySolution.case.dismissErrorsPushServiceCallOutTitle', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/case_header_page/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts b/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts new file mode 100644 index 0000000000000..8cdc287b1584c --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_BADGE_LABEL = i18n.translate( + 'xpack.securitySolution.case.caseView.pageBadgeLabel', + { + defaultMessage: 'Beta', + } +); + +export const PAGE_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.case.caseView.pageBadgeTooltip', + { + defaultMessage: + 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/case_status/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/case_status/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/case_view/actions.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/actions.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/case_view/actions.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/case_view/actions.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/case_view/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/actions.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/case_view/actions.tsx rename to x-pack/plugins/security_solution/public/cases/components/case_view/actions.tsx diff --git a/x-pack/plugins/siem/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/case_view/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx new file mode 100644 index 0000000000000..bb63cf633747b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -0,0 +1,392 @@ +/* + * 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 { + EuiButtonToggle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiLoadingSpinner, + EuiHorizontalRule, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; +import { Case } from '../../containers/types'; +import { getCaseUrl } from '../../../common/components/link_to'; +import { gutterTimeline } from '../../../common/lib/helpers'; +import { HeaderPage } from '../../../common/components/header_page'; +import { EditableTitle } from '../../../common/components/header_page/editable_title'; +import { TagList } from '../tag_list'; +import { useGetCase } from '../../containers/use_get_case'; +import { UserActionTree } from '../user_action_tree'; +import { UserList } from '../user_list'; +import { useUpdateCase } from '../../containers/use_update_case'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { getTypedPayload } from '../../containers/utils'; +import { WhitePageWrapper, HeaderWrapper } from '../wrappers'; +import { useBasePath } from '../../../common/lib/kibana'; +import { CaseStatus } from '../case_status'; +import { navTabs } from '../../../app/home/home_navigations'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; +import { usePushToService } from '../use_push_to_service'; +import { EditConnector } from '../edit_connector'; +import { useConnectors } from '../../containers/configure/use_connectors'; + +interface Props { + caseId: string; + userCanCrud: boolean; +} + +const MyWrapper = styled.div` + padding: ${({ + theme, + }) => `${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l} + ${theme.eui.paddingSizes.l}`}; +`; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const MyEuiHorizontalRule = styled(EuiHorizontalRule)` + margin-left: 48px; + &.euiHorizontalRule--full { + width: calc(100% - 48px); + } +`; + +export interface CaseProps extends Props { + fetchCase: () => void; + caseData: Case; + updateCase: (newCase: Case) => void; +} + +export const CaseComponent = React.memo( + ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { + const basePath = window.location.origin + useBasePath(); + const caseLink = `${basePath}/app/security#/case/${caseId}`; + const search = useGetUrlSearch(navTabs.case); + const [initLoadingData, setInitLoadingData] = useState(true); + const { + caseUserActions, + fetchCaseUserActions, + caseServices, + hasDataToPush, + isLoading: isLoadingUserActions, + participants, + } = useGetCaseUserActions(caseId, caseData.connectorId); + const { isLoading, updateKey, updateCaseProperty } = useUpdateCase({ + caseId, + }); + + // Update Fields + const onUpdateField = useCallback( + (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { + const handleUpdateNewCase = (newCase: Case) => + updateCase({ ...newCase, comments: caseData.comments }); + switch (newUpdateKey) { + case 'title': + const titleUpdate = getTypedPayload(updateValue); + if (titleUpdate.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'title', + updateValue: titleUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'connectorId': + const connectorId = getTypedPayload(updateValue); + if (connectorId.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'connector_id', + updateValue: connectorId, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'description': + const descriptionUpdate = getTypedPayload(updateValue); + if (descriptionUpdate.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'description', + updateValue: descriptionUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'tags': + const tagsUpdate = getTypedPayload(updateValue); + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'tags', + updateValue: tagsUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + break; + case 'status': + const statusUpdate = getTypedPayload(updateValue); + if (caseData.status !== updateValue) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'status', + updateValue: statusUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + default: + return null; + } + }, + [fetchCaseUserActions, updateCaseProperty, updateCase, caseData] + ); + const handleUpdateCase = useCallback( + (newCase: Case) => { + updateCase(newCase); + fetchCaseUserActions(newCase.id); + }, + [updateCase, fetchCaseUserActions] + ); + + const { loading: isLoadingConnectors, connectors } = useConnectors(); + + const [caseConnectorName, isValidConnector] = useMemo(() => { + const connector = connectors.find((c) => c.id === caseData.connectorId); + return [connector?.name ?? 'none', !!connector]; + }, [connectors, caseData.connectorId]); + + const currentExternalIncident = useMemo( + () => + caseServices != null && caseServices[caseData.connectorId] != null + ? caseServices[caseData.connectorId] + : null, + [caseServices, caseData.connectorId] + ); + + const { pushButton, pushCallouts } = usePushToService({ + caseConnectorId: caseData.connectorId, + caseConnectorName, + caseServices, + caseId: caseData.id, + caseStatus: caseData.status, + connectors, + updateCase: handleUpdateCase, + userCanCrud, + isValidConnector, + }); + + const onSubmitConnector = useCallback( + (connectorId) => onUpdateField('connectorId', connectorId), + [onUpdateField] + ); + const onSubmitTags = useCallback((newTags) => onUpdateField('tags', newTags), [onUpdateField]); + const onSubmitTitle = useCallback((newTitle) => onUpdateField('title', newTitle), [ + onUpdateField, + ]); + const toggleStatusCase = useCallback( + (e) => onUpdateField('status', e.target.checked ? 'closed' : 'open'), + [onUpdateField] + ); + const handleRefresh = useCallback(() => { + fetchCaseUserActions(caseData.id); + fetchCase(); + }, [caseData.id, fetchCase, fetchCaseUserActions]); + + const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); + + const caseStatusData = useMemo( + () => + caseData.status === 'open' + ? { + 'data-test-subj': 'case-view-createdAt', + value: caseData.createdAt, + title: i18n.CASE_OPENED, + buttonLabel: i18n.CLOSE_CASE, + status: caseData.status, + icon: 'folderCheck', + badgeColor: 'secondary', + isSelected: false, + } + : { + 'data-test-subj': 'case-view-closedAt', + value: caseData.closedAt ?? '', + title: i18n.CASE_CLOSED, + buttonLabel: i18n.REOPEN_CASE, + status: caseData.status, + icon: 'folderExclamation', + badgeColor: 'danger', + isSelected: true, + }, + [caseData.closedAt, caseData.createdAt, caseData.status] + ); + const emailContent = useMemo( + () => ({ + subject: i18n.EMAIL_SUBJECT(caseData.title), + body: i18n.EMAIL_BODY(caseLink), + }), + [caseLink, caseData.title] + ); + + useEffect(() => { + if (initLoadingData && !isLoadingUserActions) { + setInitLoadingData(false); + } + }, [initLoadingData, isLoadingUserActions]); + + const backOptions = useMemo( + () => ({ + href: getCaseUrl(search), + text: i18n.BACK_TO_ALL, + dataTestSubj: 'backToCases', + }), + [search] + ); + + return ( + <> + + + } + title={caseData.title} + > + + + + + + {!initLoadingData && pushCallouts != null && pushCallouts} + + + {initLoadingData && } + {!initLoadingData && ( + <> + + + + + + + {hasDataToPush && ( + + {pushButton} + + )} + + + )} + + + + + + + + + + + + + ); + } +); + +export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { + const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId); + if (isError) { + return null; + } + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + ); +}); + +CaseComponent.displayName = 'CaseComponent'; +CaseView.displayName = 'CaseView'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts b/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts new file mode 100644 index 0000000000000..b8219ad52f5b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const SHOWING_CASES = (actionDate: string, actionName: string, userName: string) => + i18n.translate('xpack.securitySolution.case.caseView.actionHeadline', { + values: { + actionDate, + actionName, + userName, + }, + defaultMessage: '{userName} {actionName} on {actionDate}', + }); + +export const ADDED_FIELD = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.addedField', + { + defaultMessage: 'added', + } +); + +export const CHANGED_FIELD = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.changededField', + { + defaultMessage: 'changed', + } +); + +export const SELECTED_THIRD_PARTY = (thirdParty: string) => + i18n.translate('xpack.securitySolution.case.caseView.actionLabel.selectedThirdParty', { + values: { + thirdParty, + }, + defaultMessage: 'selected { thirdParty } as incident management system', + }); + +export const REMOVED_THIRD_PARTY = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.removedThirdParty', + { + defaultMessage: 'removed external incident management system', + } +); + +export const EDITED_FIELD = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.editedField', + { + defaultMessage: 'edited', + } +); + +export const REMOVED_FIELD = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.removedField', + { + defaultMessage: 'removed', + } +); + +export const VIEW_INCIDENT = (incidentNumber: string) => + i18n.translate('xpack.securitySolution.case.caseView.actionLabel.viewIncident', { + defaultMessage: 'View {incidentNumber}', + values: { + incidentNumber, + }, + }); + +export const PUSHED_NEW_INCIDENT = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.pushedNewIncident', + { + defaultMessage: 'pushed as new incident', + } +); + +export const UPDATE_INCIDENT = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.updateIncident', + { + defaultMessage: 'updated incident', + } +); + +export const ADDED_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.addDescription', + { + defaultMessage: 'added description', + } +); + +export const EDIT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.case.caseView.edit.description', + { + defaultMessage: 'Edit description', + } +); + +export const QUOTE = i18n.translate('xpack.securitySolution.case.caseView.edit.quote', { + defaultMessage: 'Quote', +}); + +export const EDIT_COMMENT = i18n.translate('xpack.securitySolution.case.caseView.edit.comment', { + defaultMessage: 'Edit comment', +}); + +export const ON = i18n.translate('xpack.securitySolution.case.caseView.actionLabel.on', { + defaultMessage: 'on', +}); + +export const ADDED_COMMENT = i18n.translate( + 'xpack.securitySolution.case.caseView.actionLabel.addComment', + { + defaultMessage: 'added comment', + } +); + +export const STATUS = i18n.translate('xpack.securitySolution.case.caseView.statusLabel', { + defaultMessage: 'Status', +}); + +export const CASE = i18n.translate('xpack.securitySolution.case.caseView.case', { + defaultMessage: 'case', +}); + +export const COMMENT = i18n.translate('xpack.securitySolution.case.caseView.comment', { + defaultMessage: 'comment', +}); + +export const CASE_OPENED = i18n.translate('xpack.securitySolution.case.caseView.caseOpened', { + defaultMessage: 'Case opened', +}); + +export const CASE_CLOSED = i18n.translate('xpack.securitySolution.case.caseView.caseClosed', { + defaultMessage: 'Case closed', +}); + +export const CASE_REFRESH = i18n.translate('xpack.securitySolution.case.caseView.caseRefresh', { + defaultMessage: 'Refresh case', +}); + +export const EMAIL_SUBJECT = (caseTitle: string) => + i18n.translate('xpack.securitySolution.case.caseView.emailSubject', { + values: { caseTitle }, + defaultMessage: 'Security Case - {caseTitle}', + }); + +export const EMAIL_BODY = (caseUrl: string) => + i18n.translate('xpack.securitySolution.case.caseView.emailBody', { + values: { caseUrl }, + defaultMessage: 'Case reference: {caseUrl}', + }); +export const UNKNOWN = i18n.translate('xpack.securitySolution.case.caseView.unknown', { + defaultMessage: 'Unknown', +}); diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/__mock__/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/button.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/button.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/button.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options_radio.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options_radio.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options_radio.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options_radio.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx index 43d5351a5dce3..67963c7487826 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx @@ -52,7 +52,7 @@ describe('FieldMappingRow', () => { test('it pass the corrects props to mapping row', () => { const rows = wrapper.find(FieldMappingRow); rows.forEach((row, index) => { - expect(row.prop('siemField')).toEqual(mapping[index].source); + expect(row.prop('securitySolutionField')).toEqual(mapping[index].source); expect(row.prop('selectedActionType')).toEqual(mapping[index].actionType); expect(row.prop('selectedThirdParty')).toEqual(mapping[index].target); }); @@ -68,7 +68,7 @@ describe('FieldMappingRow', () => { const rows = newWrapper.find(FieldMappingRow); rows.forEach((row, index) => { - expect(row.prop('siemField')).toEqual(defaultMapping[index].source); + expect(row.prop('securitySolutionField')).toEqual(defaultMapping[index].source); expect(row.prop('selectedActionType')).toEqual(defaultMapping[index].actionType); expect(row.prop('selectedThirdParty')).toEqual(defaultMapping[index].target); }); diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx similarity index 99% rename from x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx index 3d515941fc2f3..415faa96eeedd 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx @@ -132,7 +132,7 @@ const FieldMappingComponent: React.FC = ({ key={`${item.source}`} id={`${item.source}`} disabled={disabled} - siemField={item.source} + securitySolutionField={item.source} thirdPartyOptions={getThirdPartyOptions(item.source, selectedConnector.fields)} actionTypeOptions={actionTypeOptions} onChangeActionType={onChangeActionType} diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx index 3787a43ff2d28..a2acd0e20b6ad 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx @@ -51,7 +51,7 @@ describe('FieldMappingRow', () => { const props: RowProps = { id: 'title', disabled: false, - siemField: 'title', + securitySolutionField: 'title', thirdPartyOptions, actionTypeOptions, onChangeActionType, diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx similarity index 85% rename from x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx index 922ea7222efce..6e688b213f82c 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx @@ -20,7 +20,7 @@ import { AllThirdPartyFields } from '../../../common/lib/connectors/types'; export interface RowProps { id: string; disabled: boolean; - siemField: CaseField; + securitySolutionField: CaseField; thirdPartyOptions: Array>; actionTypeOptions: Array>; onChangeActionType: (caseField: CaseField, newActionType: ActionType) => void; @@ -32,7 +32,7 @@ export interface RowProps { const FieldMappingRowComponent: React.FC = ({ id, disabled, - siemField, + securitySolutionField, thirdPartyOptions, actionTypeOptions, onChangeActionType, @@ -40,13 +40,15 @@ const FieldMappingRowComponent: React.FC = ({ selectedActionType, selectedThirdParty, }) => { - const siemFieldCapitalized = useMemo(() => capitalize(siemField), [siemField]); + const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ + securitySolutionField, + ]); return ( - {siemFieldCapitalized} + {securitySolutionFieldCapitalized} @@ -58,7 +60,7 @@ const FieldMappingRowComponent: React.FC = ({ disabled={disabled} options={thirdPartyOptions} valueOfSelected={selectedThirdParty} - onChange={onChangeThirdParty.bind(null, siemField)} + onChange={onChangeThirdParty.bind(null, securitySolutionField)} data-test-subj={`case-configure-third-party-select-${id}`} /> @@ -67,7 +69,7 @@ const FieldMappingRowComponent: React.FC = ({ disabled={disabled} options={actionTypeOptions} valueOfSelected={selectedActionType} - onChange={onChangeActionType.bind(null, siemField)} + onChange={onChangeActionType.bind(null, securitySolutionField)} data-test-subj={`case-configure-action-type-select-${id}`} /> diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/mapping.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/mapping.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/mapping.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/mapping.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts new file mode 100644 index 0000000000000..9ef6ce2f3d4a9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( + 'xpack.securitySolution.case.configureCases.incidentManagementSystemTitle', + { + defaultMessage: 'Connect to external incident management system', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( + 'xpack.securitySolution.case.configureCases.incidentManagementSystemDesc', + { + defaultMessage: + 'You may optionally connect Security cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( + 'xpack.securitySolution.case.configureCases.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); + +export const NO_CONNECTOR = i18n.translate( + 'xpack.securitySolution.case.configureCases.noConnector', + { + defaultMessage: 'No connector selected', + } +); + +export const ADD_NEW_CONNECTOR = i18n.translate( + 'xpack.securitySolution.case.configureCases.addNewConnector', + { + defaultMessage: 'Add new connector', + } +); + +export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( + 'xpack.securitySolution.case.configureCases.caseClosureOptionsTitle', + { + defaultMessage: 'Case Closures', + } +); + +export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( + 'xpack.securitySolution.case.configureCases.caseClosureOptionsDesc', + { + defaultMessage: + 'Define how you wish Security cases to be closed. Automated case closures require an established connection to an external incident management system.', + } +); + +export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( + 'xpack.securitySolution.case.configureCases.caseClosureOptionsLabel', + { + defaultMessage: 'Case closure options', + } +); + +export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( + 'xpack.securitySolution.case.configureCases.caseClosureOptionsManual', + { + defaultMessage: 'Manually close Security cases', + } +); + +export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( + 'xpack.securitySolution.case.configureCases.caseClosureOptionsNewIncident', + { + defaultMessage: + 'Automatically close Security cases when pushing new incident to external system', + } +); + +export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( + 'xpack.securitySolution.case.configureCases.caseClosureOptionsClosedIncident', + { + defaultMessage: 'Automatically close Security cases when incident is closed in external system', + } +); + +export const FIELD_MAPPING_TITLE = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingTitle', + { + defaultMessage: 'Field mappings', + } +); + +export const FIELD_MAPPING_DESC = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingDesc', + { + defaultMessage: + 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', + } +); + +export const FIELD_MAPPING_FIRST_COL = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingFirstCol', + { + defaultMessage: 'Security case field', + } +); + +export const FIELD_MAPPING_SECOND_COL = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingSecondCol', + { + defaultMessage: 'External incident field', + } +); + +export const FIELD_MAPPING_THIRD_COL = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingThirdCol', + { + defaultMessage: 'On edit and update', + } +); + +export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingEditNothing', + { + defaultMessage: 'Nothing', + } +); + +export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingEditOverwrite', + { + defaultMessage: 'Overwrite', + } +); + +export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( + 'xpack.securitySolution.case.configureCases.fieldMappingEditAppend', + { + defaultMessage: 'Append', + } +); + +export const CANCEL = i18n.translate('xpack.securitySolution.case.configureCases.cancelButton', { + defaultMessage: 'Cancel', +}); + +export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( + 'xpack.securitySolution.case.configureCases.warningTitle', + { + defaultMessage: 'Warning', + } +); + +export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( + 'xpack.securitySolution.case.configureCases.warningMessage', + { + defaultMessage: + 'The selected connector has been deleted. Either select a different connector or create a new one.', + } +); + +export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( + 'xpack.securitySolution.case.configureCases.mappingFieldNotMapped', + { + defaultMessage: 'Not mapped', + } +); + +export const UPDATE_CONNECTOR = i18n.translate( + 'xpack.securitySolution.case.configureCases.updateConnector', + { + defaultMessage: 'Update connector', + } +); + +export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.updateSelectedConnector', { + values: { connectorName }, + defaultMessage: 'Update { connectorName }', + }); +}; diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/utils.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/utils.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/utils.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/components/configure_cases/utils.ts rename to x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts diff --git a/x-pack/plugins/siem/public/cases/components/confirm_delete_case/index.tsx b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/confirm_delete_case/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/translations.ts b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/translations.ts new file mode 100644 index 0000000000000..ad51d13206809 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +export * from '../../translations'; + +export const DELETE_TITLE = (caseTitle: string) => + i18n.translate('xpack.securitySolution.case.confirmDeleteCase.deleteTitle', { + values: { caseTitle }, + defaultMessage: 'Delete "{caseTitle}"', + }); + +export const CONFIRM_QUESTION = i18n.translate( + 'xpack.securitySolution.case.confirmDeleteCase.confirmQuestion', + { + defaultMessage: + 'By deleting this case, all related case data will be permanently removed and you will no longer be able to push data to an external incident management system. Are you sure you wish to proceed?', + } +); +export const DELETE_SELECTED_CASES = i18n.translate( + 'xpack.securitySolution.case.confirmDeleteCase.selectedCases', + { + defaultMessage: 'Delete selected cases', + } +); + +export const CONFIRM_QUESTION_PLURAL = i18n.translate( + 'xpack.securitySolution.case.confirmDeleteCase.confirmQuestionPlural', + { + defaultMessage: + 'By deleting these cases, all related case data will be permanently removed and you will no longer be able to push data to an external incident management system. Are you sure you wish to proceed?', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/connector_selector/form.tsx rename to x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx diff --git a/x-pack/plugins/siem/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/create/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/create/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/create/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/create/optional_field_label/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/create/optional_field_label/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/create/schema.tsx rename to x-pack/plugins/security_solution/public/cases/components/create/schema.tsx diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/schema.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/edit_connector/schema.tsx rename to x-pack/plugins/security_solution/public/cases/components/edit_connector/schema.tsx diff --git a/x-pack/plugins/siem/public/cases/components/filter_popover/index.tsx b/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/filter_popover/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/open_closed_stats/index.tsx b/x-pack/plugins/security_solution/public/cases/components/open_closed_stats/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/open_closed_stats/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/open_closed_stats/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/property_actions/constants.ts b/x-pack/plugins/security_solution/public/cases/components/property_actions/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/components/property_actions/constants.ts rename to x-pack/plugins/security_solution/public/cases/components/property_actions/constants.ts diff --git a/x-pack/plugins/siem/public/cases/components/property_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/property_actions/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/property_actions/translations.ts b/x-pack/plugins/security_solution/public/cases/components/property_actions/translations.ts new file mode 100644 index 0000000000000..a00abcc7845b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/property_actions/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ACTIONS_ARIA = i18n.translate( + 'xpack.securitySolution.case.caseView.editActionsLinkAria', + { + defaultMessage: 'click to see all actions', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/tag_list/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/tag_list/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/tag_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/tag_list/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/tag_list/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/schema.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/tag_list/schema.tsx rename to x-pack/plugins/security_solution/public/cases/components/tag_list/schema.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/translations.ts b/x-pack/plugins/security_solution/public/cases/components/tag_list/translations.ts new file mode 100644 index 0000000000000..3aeb6391267f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/translations.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const EDIT_TAGS_ARIA = i18n.translate( + 'xpack.securitySolution.case.caseView.editTagsLinkAria', + { + defaultMessage: 'click to edit tags', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx new file mode 100644 index 0000000000000..919231d2f6034 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +import * as i18n from './translations'; +import { ActionLicense } from '../../containers/types'; + +export const getLicenseError = () => ({ + title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE, + description: ( + + {i18n.LINK_CLOUD_DEPLOYMENT} +
+ ), + }} + /> + ), +}); + +export const getKibanaConfigError = () => ({ + title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE, + description: ( + + {'coming soon...'} +
+ ), + }} + /> + ), +}); + +export const getActionLicenseError = ( + actionLicense: ActionLicense | null +): Array<{ title: string; description: JSX.Element }> => { + let errors: Array<{ title: string; description: JSX.Element }> = []; + if (actionLicense != null && !actionLicense.enabledInLicense) { + errors = [...errors, getLicenseError()]; + } + if (actionLicense != null && !actionLicense.enabledInConfig) { + errors = [...errors, getKibanaConfigError()]; + } + return errors; +}; diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx new file mode 100644 index 0000000000000..2e9e3cce4306a --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx @@ -0,0 +1,197 @@ +/* + * 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 { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; + +import { Case } from '../../containers/types'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; +import { usePostPushToService } from '../../containers/use_post_push_to_service'; +import { getConfigureCasesUrl } from '../../../common/components/link_to'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; +import { CaseCallOut } from '../callout'; +import { getLicenseError, getKibanaConfigError } from './helpers'; +import * as i18n from './translations'; +import { Connector } from '../../../../../case/common/api/cases'; +import { CaseServices } from '../../containers/use_get_case_user_actions'; + +export interface UsePushToService { + caseId: string; + caseStatus: string; + caseConnectorId: string; + caseConnectorName: string; + caseServices: CaseServices; + connectors: Connector[]; + updateCase: (newCase: Case) => void; + userCanCrud: boolean; + isValidConnector: boolean; +} + +export interface ReturnUsePushToService { + pushButton: JSX.Element; + pushCallouts: JSX.Element | null; +} + +export const usePushToService = ({ + caseConnectorId, + caseConnectorName, + caseId, + caseServices, + caseStatus, + connectors, + updateCase, + userCanCrud, + isValidConnector, +}: UsePushToService): ReturnUsePushToService => { + const urlSearch = useGetUrlSearch(navTabs.case); + + const { isLoading, postPushToService } = usePostPushToService(); + + const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); + + const handlePushToService = useCallback(() => { + if (caseConnectorId != null && caseConnectorId !== 'none') { + postPushToService({ + caseId, + caseServices, + connectorId: caseConnectorId, + connectorName: caseConnectorName, + updateCase, + }); + } + }, [caseId, caseServices, caseConnectorId, caseConnectorName, postPushToService, updateCase]); + + const errorsMsg = useMemo(() => { + let errors: Array<{ + title: string; + description: JSX.Element; + errorType?: 'primary' | 'success' | 'warning' | 'danger'; + }> = []; + if (actionLicense != null && !actionLicense.enabledInLicense) { + errors = [...errors, getLicenseError()]; + } + if (connectors.length === 0 && caseConnectorId === 'none' && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CONFIG_TITLE, + description: ( + + {i18n.LINK_CONNECTOR_CONFIGURE} + + ), + }} + /> + ), + }, + ]; + } else if (caseConnectorId === 'none' && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, + description: ( + + ), + }, + ]; + } else if (!isValidConnector && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, + description: ( + + ), + errorType: 'danger', + }, + ]; + } + if (caseStatus === 'closed') { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE, + description: ( + + ), + }, + ]; + } + if (actionLicense != null && !actionLicense.enabledInConfig) { + errors = [...errors, getKibanaConfigError()]; + } + return errors; + }, [actionLicense, caseStatus, connectors.length, caseConnectorId, loadingLicense, urlSearch]); + + const pushToServiceButton = useMemo(() => { + return ( + 0 || !userCanCrud || !isValidConnector + } + isLoading={isLoading} + > + {caseServices[caseConnectorId] + ? i18n.UPDATE_THIRD(caseConnectorName) + : i18n.PUSH_THIRD(caseConnectorName)} + + ); + }, [ + caseConnectorId, + caseConnectorName, + connectors, + errorsMsg, + handlePushToService, + isLoading, + loadingLicense, + userCanCrud, + isValidConnector, + ]); + + const objToReturn = useMemo(() => { + return { + pushButton: + errorsMsg.length > 0 ? ( + {errorsMsg[0].description}

} + > + {pushToServiceButton} +
+ ) : ( + <>{pushToServiceButton} + ), + pushCallouts: + errorsMsg.length > 0 ? ( + + ) : null, + }; + }, [errorsMsg, pushToServiceButton]); + + return objToReturn; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/translations.ts b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/translations.ts new file mode 100644 index 0000000000000..f4539b8019d43 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/translations.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_PUSH_SERVICE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseView.errorsPushServiceCallOutTitle', + { + defaultMessage: 'To send cases to external systems, you need to:', + } +); +export const PUSH_THIRD = (thirdParty: string) => { + if (thirdParty === 'none') { + return i18n.translate('xpack.securitySolution.case.caseView.pushThirdPartyIncident', { + defaultMessage: 'Push as external incident', + }); + } + + return i18n.translate('xpack.securitySolution.case.caseView.pushNamedIncident', { + values: { thirdParty }, + defaultMessage: 'Push as { thirdParty } incident', + }); +}; + +export const UPDATE_THIRD = (thirdParty: string) => { + if (thirdParty === 'none') { + return i18n.translate('xpack.securitySolution.case.caseView.updateThirdPartyIncident', { + defaultMessage: 'Update external incident', + }); + } + + return i18n.translate('xpack.securitySolution.case.caseView.updateNamedIncident', { + values: { thirdParty }, + defaultMessage: 'Update { thirdParty } incident', + }); +}; + +export const PUSH_DISABLE_BY_NO_CONFIG_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseView.pushToServiceDisableByNoConfigTitle', + { + defaultMessage: 'Configure external connector', + } +); + +export const PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseView.pushToServiceDisableByNoCaseConfigTitle', + { + defaultMessage: 'Select external connector', + } +); + +export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseView.pushToServiceDisableBecauseCaseClosedTitle', + { + defaultMessage: 'Reopen the case', + } +); + +export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseView.pushToServiceDisableByConfigTitle', + { + defaultMessage: 'Enable external service in Kibana configuration file', + } +); + +export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseView.pushToServiceDisableByLicenseTitle', + { + defaultMessage: 'Upgrade to Elastic Platinum', + } +); + +export const LINK_CLOUD_DEPLOYMENT = i18n.translate( + 'xpack.securitySolution.case.caseView.cloudDeploymentLink', + { + defaultMessage: 'cloud deployment', + } +); + +export const LINK_CONNECTOR_CONFIGURE = i18n.translate( + 'xpack.securitySolution.case.caseView.connectorConfigureLink', + { + defaultMessage: 'connector', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/schema.ts b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/schema.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/schema.ts rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/schema.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/translations.ts b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/translations.ts new file mode 100644 index 0000000000000..73c94477b4e73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../case_view/translations'; + +export const ALREADY_PUSHED_TO_SERVICE = (externalService: string) => + i18n.translate('xpack.securitySolution.case.caseView.alreadyPushedToExternalService', { + values: { externalService }, + defaultMessage: 'Already pushed to { externalService } incident', + }); + +export const REQUIRED_UPDATE_TO_SERVICE = (externalService: string) => + i18n.translate('xpack.securitySolution.case.caseView.requiredUpdateToExternalService', { + values: { externalService }, + defaultMessage: 'Requires update to { externalService } incident', + }); + +export const COPY_REFERENCE_LINK = i18n.translate( + 'xpack.securitySolution.case.caseView.copyCommentLinkAria', + { + defaultMessage: 'Copy reference link', + } +); + +export const MOVE_TO_ORIGINAL_COMMENT = i18n.translate( + 'xpack.securitySolution.case.caseView.moveToCommentAria', + { + defaultMessage: 'Highlight the referenced comment', + } +); diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_avatar.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_avatar.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_item.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_item.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx index 8e07706d9c6ba..ae9f1ec7469e4 100644 --- a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx @@ -18,7 +18,7 @@ const onSaveContent = jest.fn(); const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c'; const defaultProps = { - content: `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`, + content: `A link to a timeline [timeline](http://localhost:5601/app/security#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`, id: 'markdown-id', isEditable: false, onChangeEditable, diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_list/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_list/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_list/index.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_list/index.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_list/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/user_list/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/user_list/index.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/user_list/translations.ts b/x-pack/plugins/security_solution/public/cases/components/user_list/translations.ts new file mode 100644 index 0000000000000..3439d58eb41f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/user_list/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const SEND_EMAIL_ARIA = (user: string) => + i18n.translate('xpack.securitySolution.case.caseView.sendEmalLinkAria', { + values: { user }, + defaultMessage: 'click to send an email to {user}', + }); diff --git a/x-pack/plugins/siem/public/cases/components/wrappers/index.tsx b/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/components/wrappers/index.tsx rename to x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/api.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/api.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/api.ts rename to x-pack/plugins/security_solution/public/cases/containers/api.ts diff --git a/x-pack/plugins/siem/public/cases/containers/configure/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/cases/containers/configure/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/cases/containers/configure/api.test.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/api.test.ts rename to x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts diff --git a/x-pack/plugins/siem/public/cases/containers/configure/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/api.ts rename to x-pack/plugins/security_solution/public/cases/containers/configure/api.ts diff --git a/x-pack/plugins/siem/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/mock.ts rename to x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/translations.ts new file mode 100644 index 0000000000000..ee21167021e19 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/translations.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const SUCCESS_CONFIGURE = i18n.translate( + 'xpack.securitySolution.case.configure.successSaveToast', + { + defaultMessage: 'Saved external connection settings', + } +); diff --git a/x-pack/plugins/siem/public/cases/containers/configure/types.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/types.ts rename to x-pack/plugins/security_solution/public/cases/containers/configure/types.ts diff --git a/x-pack/plugins/siem/public/cases/containers/configure/use_configure.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/use_configure.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/use_configure.tsx rename to x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/configure/use_connectors.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/use_connectors.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/configure/use_connectors.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/configure/use_connectors.tsx rename to x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/constants.ts b/x-pack/plugins/security_solution/public/cases/containers/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/constants.ts rename to x-pack/plugins/security_solution/public/cases/containers/constants.ts diff --git a/x-pack/plugins/siem/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/mock.ts rename to x-pack/plugins/security_solution/public/cases/containers/mock.ts diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts new file mode 100644 index 0000000000000..96d2ec2f874db --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_TITLE = i18n.translate('xpack.securitySolution.containers.case.errorTitle', { + defaultMessage: 'Error fetching data', +}); + +export const ERROR_DELETING = i18n.translate( + 'xpack.securitySolution.containers.case.errorDeletingTitle', + { + defaultMessage: 'Error deleting data', + } +); + +export const UPDATED_CASE = (caseTitle: string) => + i18n.translate('xpack.securitySolution.containers.case.updatedCase', { + values: { caseTitle }, + defaultMessage: 'Updated "{caseTitle}"', + }); + +export const DELETED_CASES = (totalCases: number, caseTitle?: string) => + i18n.translate('xpack.securitySolution.containers.case.deletedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const CLOSED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.securitySolution.containers.case.closedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const REOPENED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.securitySolution.containers.case.reopenedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => + i18n.translate('xpack.securitySolution.containers.case.pushToExternalService', { + values: { serviceName }, + defaultMessage: 'Successfully sent to { serviceName }', + }); + +export const ERROR_PUSH_TO_SERVICE = i18n.translate( + 'xpack.securitySolution.case.configure.errorPushingToService', + { + defaultMessage: 'Error pushing to service', + } +); diff --git a/x-pack/plugins/siem/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/types.ts rename to x-pack/plugins/security_solution/public/cases/containers/types.ts diff --git a/x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_delete_cases.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_delete_cases.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_delete_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_delete_cases.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_action_license.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_action_license.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_action_license.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_action_license.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_case.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_case.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_cases.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_cases.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_cases.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_cases_status.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_cases_status.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_cases_status.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_cases_status.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_reporters.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_reporters.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_reporters.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_reporters.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_tags.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_tags.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_tags.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_get_tags.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_get_tags.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_post_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_post_case.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_post_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_post_case.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_post_comment.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_post_comment.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_update_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_update_case.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_update_case.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_update_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_update_comment.test.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/use_update_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/use_update_comment.tsx rename to x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx diff --git a/x-pack/plugins/siem/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/containers/utils.ts rename to x-pack/plugins/security_solution/public/cases/containers/utils.ts diff --git a/x-pack/plugins/siem/public/cases/index.ts b/x-pack/plugins/security_solution/public/cases/index.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/index.ts rename to x-pack/plugins/security_solution/public/cases/index.ts diff --git a/x-pack/plugins/siem/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/pages/case.tsx rename to x-pack/plugins/security_solution/public/cases/pages/case.tsx diff --git a/x-pack/plugins/siem/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/pages/case_details.tsx rename to x-pack/plugins/security_solution/public/cases/pages/case_details.tsx diff --git a/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/pages/configure_cases.tsx rename to x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx diff --git a/x-pack/plugins/siem/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/pages/create_case.tsx rename to x-pack/plugins/security_solution/public/cases/pages/create_case.tsx diff --git a/x-pack/plugins/siem/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/pages/index.tsx rename to x-pack/plugins/security_solution/public/cases/pages/index.tsx diff --git a/x-pack/plugins/siem/public/cases/pages/saved_object_no_permissions.tsx b/x-pack/plugins/security_solution/public/cases/pages/saved_object_no_permissions.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/pages/saved_object_no_permissions.tsx rename to x-pack/plugins/security_solution/public/cases/pages/saved_object_no_permissions.tsx diff --git a/x-pack/plugins/security_solution/public/cases/pages/translations.ts b/x-pack/plugins/security_solution/public/cases/pages/translations.ts new file mode 100644 index 0000000000000..5b595c5892ef2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/pages/translations.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseSavedObjectNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.securitySolution.case.caseSavedObjectNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); + +export const BACK_TO_ALL = i18n.translate('xpack.securitySolution.case.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.securitySolution.case.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const DELETE_CASE = i18n.translate( + 'xpack.securitySolution.case.confirmDeleteCase.deleteCase', + { + defaultMessage: 'Delete case', + } +); + +export const DELETE_CASES = i18n.translate( + 'xpack.securitySolution.case.confirmDeleteCase.deleteCases', + { + defaultMessage: 'Delete cases', + } +); + +export const NAME = i18n.translate('xpack.securitySolution.case.caseView.name', { + defaultMessage: 'Name', +}); + +export const OPENED_ON = i18n.translate('xpack.securitySolution.case.caseView.openedOn', { + defaultMessage: 'Opened on', +}); + +export const CLOSED_ON = i18n.translate('xpack.securitySolution.case.caseView.closedOn', { + defaultMessage: 'Closed on', +}); + +export const REPORTER = i18n.translate('xpack.securitySolution.case.caseView.reporterLabel', { + defaultMessage: 'Reporter', +}); + +export const PARTICIPANTS = i18n.translate( + 'xpack.securitySolution.case.caseView.particpantsLabel', + { + defaultMessage: 'Participants', + } +); + +export const CREATE_BC_TITLE = i18n.translate('xpack.securitySolution.case.caseView.breadcrumb', { + defaultMessage: 'Create', +}); + +export const CREATE_TITLE = i18n.translate('xpack.securitySolution.case.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.securitySolution.case.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const COMMENT_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.caseView.commentFieldRequiredError', + { + defaultMessage: 'A comment is required.', + } +); + +export const REQUIRED_FIELD = i18n.translate( + 'xpack.securitySolution.case.caseView.fieldRequiredError', + { + defaultMessage: 'Required field', + } +); + +export const EDIT = i18n.translate('xpack.securitySolution.case.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.securitySolution.case.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const PAGE_TITLE = i18n.translate('xpack.securitySolution.case.pageTitle', { + defaultMessage: 'Cases', +}); + +export const CREATE_CASE = i18n.translate('xpack.securitySolution.case.caseView.createCase', { + defaultMessage: 'Create case', +}); + +export const CLOSED_CASE = i18n.translate('xpack.securitySolution.case.caseView.closedCase', { + defaultMessage: 'Closed case', +}); + +export const CLOSE_CASE = i18n.translate('xpack.securitySolution.case.caseView.closeCase', { + defaultMessage: 'Close case', +}); + +export const REOPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView.reopenCase', { + defaultMessage: 'Reopen case', +}); + +export const REOPENED_CASE = i18n.translate('xpack.securitySolution.case.caseView.reopenedCase', { + defaultMessage: 'Reopened case', +}); + +export const CASE_NAME = i18n.translate('xpack.securitySolution.case.caseView.caseName', { + defaultMessage: 'Case name', +}); + +export const TO = i18n.translate('xpack.securitySolution.case.caseView.to', { + defaultMessage: 'to', +}); + +export const TAGS = i18n.translate('xpack.securitySolution.case.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const ACTIONS = i18n.translate('xpack.securitySolution.case.allCases.actions', { + defaultMessage: 'Actions', +}); + +export const NO_TAGS_AVAILABLE = i18n.translate( + 'xpack.securitySolution.case.allCases.noTagsAvailable', + { + defaultMessage: 'No tags available', + } +); + +export const NO_REPORTERS_AVAILABLE = i18n.translate( + 'xpack.securitySolution.case.caseView.noReportersAvailable', + { + defaultMessage: 'No reporters available.', + } +); + +export const COMMENTS = i18n.translate('xpack.securitySolution.case.allCases.comments', { + defaultMessage: 'Comments', +}); + +export const TAGS_HELP = i18n.translate( + 'xpack.securitySolution.case.createCase.fieldTagsHelpText', + { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', + } +); + +export const NO_TAGS = i18n.translate('xpack.securitySolution.case.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.createCase.titleFieldRequiredError', + { + defaultMessage: 'A title is required.', + } +); + +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.case.configureCases.headerTitle', + { + defaultMessage: 'Configure cases', + } +); + +export const CONFIGURE_CASES_BUTTON = i18n.translate( + 'xpack.securitySolution.case.configureCasesButton', + { + defaultMessage: 'Edit external connection', + } +); + +export const ADD_COMMENT = i18n.translate( + 'xpack.securitySolution.case.caseView.comment.addComment', + { + defaultMessage: 'Add comment', + } +); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.case.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.securitySolution.case.caseView.description.save', { + defaultMessage: 'Save', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.securitySolution.case.caseView.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const CONNECTORS = i18n.translate('xpack.securitySolution.case.caseView.connectors', { + defaultMessage: 'External incident management system', +}); + +export const EDIT_CONNECTOR = i18n.translate('xpack.securitySolution.case.caseView.editConnector', { + defaultMessage: 'Change external incident management system', +}); diff --git a/x-pack/plugins/siem/public/cases/pages/utils.ts b/x-pack/plugins/security_solution/public/cases/pages/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/cases/pages/utils.ts rename to x-pack/plugins/security_solution/public/cases/pages/utils.ts diff --git a/x-pack/plugins/siem/public/cases/routes.tsx b/x-pack/plugins/security_solution/public/cases/routes.tsx similarity index 100% rename from x-pack/plugins/siem/public/cases/routes.tsx rename to x-pack/plugins/security_solution/public/cases/routes.tsx diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts new file mode 100644 index 0000000000000..5b595c5892ef2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseSavedObjectNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.securitySolution.case.caseSavedObjectNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); + +export const BACK_TO_ALL = i18n.translate('xpack.securitySolution.case.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.securitySolution.case.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const DELETE_CASE = i18n.translate( + 'xpack.securitySolution.case.confirmDeleteCase.deleteCase', + { + defaultMessage: 'Delete case', + } +); + +export const DELETE_CASES = i18n.translate( + 'xpack.securitySolution.case.confirmDeleteCase.deleteCases', + { + defaultMessage: 'Delete cases', + } +); + +export const NAME = i18n.translate('xpack.securitySolution.case.caseView.name', { + defaultMessage: 'Name', +}); + +export const OPENED_ON = i18n.translate('xpack.securitySolution.case.caseView.openedOn', { + defaultMessage: 'Opened on', +}); + +export const CLOSED_ON = i18n.translate('xpack.securitySolution.case.caseView.closedOn', { + defaultMessage: 'Closed on', +}); + +export const REPORTER = i18n.translate('xpack.securitySolution.case.caseView.reporterLabel', { + defaultMessage: 'Reporter', +}); + +export const PARTICIPANTS = i18n.translate( + 'xpack.securitySolution.case.caseView.particpantsLabel', + { + defaultMessage: 'Participants', + } +); + +export const CREATE_BC_TITLE = i18n.translate('xpack.securitySolution.case.caseView.breadcrumb', { + defaultMessage: 'Create', +}); + +export const CREATE_TITLE = i18n.translate('xpack.securitySolution.case.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.securitySolution.case.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const COMMENT_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.caseView.commentFieldRequiredError', + { + defaultMessage: 'A comment is required.', + } +); + +export const REQUIRED_FIELD = i18n.translate( + 'xpack.securitySolution.case.caseView.fieldRequiredError', + { + defaultMessage: 'Required field', + } +); + +export const EDIT = i18n.translate('xpack.securitySolution.case.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.securitySolution.case.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const PAGE_TITLE = i18n.translate('xpack.securitySolution.case.pageTitle', { + defaultMessage: 'Cases', +}); + +export const CREATE_CASE = i18n.translate('xpack.securitySolution.case.caseView.createCase', { + defaultMessage: 'Create case', +}); + +export const CLOSED_CASE = i18n.translate('xpack.securitySolution.case.caseView.closedCase', { + defaultMessage: 'Closed case', +}); + +export const CLOSE_CASE = i18n.translate('xpack.securitySolution.case.caseView.closeCase', { + defaultMessage: 'Close case', +}); + +export const REOPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView.reopenCase', { + defaultMessage: 'Reopen case', +}); + +export const REOPENED_CASE = i18n.translate('xpack.securitySolution.case.caseView.reopenedCase', { + defaultMessage: 'Reopened case', +}); + +export const CASE_NAME = i18n.translate('xpack.securitySolution.case.caseView.caseName', { + defaultMessage: 'Case name', +}); + +export const TO = i18n.translate('xpack.securitySolution.case.caseView.to', { + defaultMessage: 'to', +}); + +export const TAGS = i18n.translate('xpack.securitySolution.case.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const ACTIONS = i18n.translate('xpack.securitySolution.case.allCases.actions', { + defaultMessage: 'Actions', +}); + +export const NO_TAGS_AVAILABLE = i18n.translate( + 'xpack.securitySolution.case.allCases.noTagsAvailable', + { + defaultMessage: 'No tags available', + } +); + +export const NO_REPORTERS_AVAILABLE = i18n.translate( + 'xpack.securitySolution.case.caseView.noReportersAvailable', + { + defaultMessage: 'No reporters available.', + } +); + +export const COMMENTS = i18n.translate('xpack.securitySolution.case.allCases.comments', { + defaultMessage: 'Comments', +}); + +export const TAGS_HELP = i18n.translate( + 'xpack.securitySolution.case.createCase.fieldTagsHelpText', + { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', + } +); + +export const NO_TAGS = i18n.translate('xpack.securitySolution.case.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.createCase.titleFieldRequiredError', + { + defaultMessage: 'A title is required.', + } +); + +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.case.configureCases.headerTitle', + { + defaultMessage: 'Configure cases', + } +); + +export const CONFIGURE_CASES_BUTTON = i18n.translate( + 'xpack.securitySolution.case.configureCasesButton', + { + defaultMessage: 'Edit external connection', + } +); + +export const ADD_COMMENT = i18n.translate( + 'xpack.securitySolution.case.caseView.comment.addComment', + { + defaultMessage: 'Add comment', + } +); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.case.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.securitySolution.case.caseView.description.save', { + defaultMessage: 'Save', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.securitySolution.case.caseView.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const CONNECTORS = i18n.translate('xpack.securitySolution.case.caseView.connectors', { + defaultMessage: 'External incident management system', +}); + +export const EDIT_CONNECTOR = i18n.translate('xpack.securitySolution.case.caseView.editConnector', { + defaultMessage: 'Change external incident management system', +}); diff --git a/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.test.tsx rename to x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.ts b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.ts rename to x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/helpers.ts diff --git a/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.tsx rename to x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/translations.ts b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/translations.ts new file mode 100644 index 0000000000000..c4f36bd7dd881 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/translations.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 { i18n } from '@kbn/i18n'; + +export const FILTER_FOR_VALUE = i18n.translate( + 'xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction', + { + defaultMessage: 'Filter for value', + } +); + +export const FILTER_OUT_VALUE = i18n.translate( + 'xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction', + { + defaultMessage: 'Filter out value', + } +); diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx similarity index 86% rename from x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx rename to x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index dd608babef48f..b19343a9f4a5c 100644 --- a/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { StatefulEventsViewer } from '../events_viewer'; -import * as i18n from './translations'; import { alertsDefaultModel } from './default_headers'; - +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import * as i18n from './translations'; export interface OwnProps { end: number; id: string; @@ -59,16 +59,17 @@ interface Props { const AlertsTableComponent: React.FC = ({ endDate, startDate, pageFilters = [] }) => { const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); - const timelineTypeContext = useMemo( - () => ({ + const { initializeTimeline } = useManageTimeline(); + + useEffect(() => { + initializeTimeline({ + id: ALERTS_TABLE_ID, documentType: i18n.ALERTS_DOCUMENT_TYPE, footerText: i18n.TOTAL_COUNT_OF_ALERTS, title: i18n.ALERTS_TABLE_TITLE, unit: i18n.UNIT, - }), - [] - ); - + }); + }, []); return ( = ({ endDate, startDate, pageFilters end={endDate} id={ALERTS_TABLE_ID} start={startDate} - timelineTypeContext={timelineTypeContext} /> ); }; diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts similarity index 100% rename from x-pack/plugins/siem/public/common/components/alerts_viewer/default_headers.ts rename to x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts similarity index 100% rename from x-pack/plugins/siem/public/common/components/alerts_viewer/histogram_configs.ts rename to x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/alerts_viewer/index.tsx rename to x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts new file mode 100644 index 0000000000000..8403050a13114 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALERTS_DOCUMENT_TYPE = i18n.translate( + 'xpack.securitySolution.alertsView.alertsDocumentType', + { + defaultMessage: 'External alerts', + } +); + +export const TOTAL_COUNT_OF_ALERTS = i18n.translate( + 'xpack.securitySolution.alertsView.totalCountOfAlerts', + { + defaultMessage: 'external alerts match the search criteria', + } +); + +export const ALERTS_TABLE_TITLE = i18n.translate( + 'xpack.securitySolution.alertsView.alertsTableTitle', + { + defaultMessage: 'External alerts', + } +); + +export const ALERTS_GRAPH_TITLE = i18n.translate( + 'xpack.securitySolution.alertsView.alertsGraphTitle', + { + defaultMessage: 'External alert count', + } +); + +export const ALERTS_STACK_BY_MODULE = i18n.translate( + 'xpack.securitySolution.alertsView.alertsStackByOptions.module', + { + defaultMessage: 'module', + } +); + +export const SHOWING = i18n.translate('xpack.securitySolution.alertsView.showing', { + defaultMessage: 'Showing', +}); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.alertsView.unit', { + values: { totalCount }, + defaultMessage: `external {totalCount, plural, =1 {alert} other {alerts}}`, + }); + +export const ERROR_FETCHING_ALERTS_DATA = i18n.translate( + 'xpack.securitySolution.alertsView.errorFetchingAlertsData', + { + defaultMessage: 'Failed to query alerts data', + } +); + +export const CATEGORY = i18n.translate('xpack.securitySolution.alertsView.categoryLabel', { + defaultMessage: 'category', +}); + +export const MODULE = i18n.translate('xpack.securitySolution.alertsView.moduleLabel', { + defaultMessage: 'module', +}); diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts similarity index 100% rename from x-pack/plugins/siem/public/common/components/alerts_viewer/types.ts rename to x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx new file mode 100644 index 0000000000000..f939cf81d1bd3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; + +import { AndOrBadge } from '..'; + +const sampleText = + 'Doggo ipsum i am bekom fat snoot wow such tempt waggy wags floofs, ruff heckin good boys and girls mlem. Ruff heckin good boys and girls mlem stop it fren borkf borking doggo very hand that feed shibe, you are doing me the shock big ol heck smol borking doggo with a long snoot for pats heckin good boys. You are doing me the shock smol borking doggo with a long snoot for pats wow very biscit, length boy. Doggo ipsum i am bekom fat snoot wow such tempt waggy wags floofs, ruff heckin good boys and girls mlem. Ruff heckin good boys and girls mlem stop it fren borkf borking doggo very hand that feed shibe, you are doing me the shock big ol heck smol borking doggo with a long snoot for pats heckin good boys.'; + +storiesOf('components/AndOrBadge', module) + .add('and', () => ( + ({ eui: euiLightVars, darkMode: true })}> + + + )) + .add('or', () => ( + ({ eui: euiLightVars, darkMode: true })}> + + + )) + .add('antennas', () => ( + ({ eui: euiLightVars, darkMode: true })}> + + + + + +

{sampleText}

+
+
+
+ )); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx new file mode 100644 index 0000000000000..ed918a59a514a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { AndOrBadge } from './'; + +describe('AndOrBadge', () => { + test('it renders top and bottom antenna bars when "includeAntennas" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); + expect(wrapper.find('EuiFlexItem[data-test-subj="andOrBadgeBarTop"]')).toHaveLength(1); + expect(wrapper.find('EuiFlexItem[data-test-subj="andOrBadgeBarBottom"]')).toHaveLength(1); + }); + + test('it renders "and" when "type" is "and"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); + expect(wrapper.find('EuiFlexItem[data-test-subj="and-or-badge-bar"]')).toHaveLength(0); + }); + + test('it renders "or" when "type" is "or"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR'); + expect(wrapper.find('EuiFlexItem[data-test-subj="and-or-badge-bar"]')).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx new file mode 100644 index 0000000000000..ba3f880d9757e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx @@ -0,0 +1,108 @@ +/* + * 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, EuiBadge, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import * as i18n from './translations'; + +const AndOrBadgeAntenna = styled(EuiFlexItem)` + ${({ theme }) => css` + background: ${theme.eui.euiColorLightShade}; + position: relative; + width: 2px; + &:after { + background: ${theme.eui.euiColorLightShade}; + content: ''; + height: 8px; + right: -4px; + position: absolute; + width: 9px; + clip-path: circle(); + } + &.topAndOrBadgeAntenna { + &:after { + top: -1px; + } + } + &.bottomAndOrBadgeAntenna { + &:after { + bottom: -1px; + } + } + &.euiFlexItem { + margin: 0 12px 0 0; + } + `} +`; + +const EuiFlexItemWrapper = styled(EuiFlexItem)` + &.euiFlexItem { + margin: 0 12px 0 0; + } +`; + +const RoundedBadge = (styled(EuiBadge)` + align-items: center; + border-radius: 100%; + display: inline-flex; + font-size: 9px; + height: 34px; + justify-content: center; + margin: 0 5px 0 5px; + padding: 7px 6px 4px 6px; + user-select: none; + width: 34px; + .euiBadge__content { + position: relative; + top: -1px; + } + .euiBadge__text { + text-overflow: clip; + } +` as unknown) as typeof EuiBadge; + +RoundedBadge.displayName = 'RoundedBadge'; + +export type AndOr = 'and' | 'or'; + +/** Displays AND / OR in a round badge */ +// Ref: https://github.com/elastic/eui/issues/1655 +export const AndOrBadge = React.memo<{ type: AndOr; includeAntennas?: boolean }>( + ({ type, includeAntennas = false }) => { + const getBadge = () => ( + + {type === 'and' ? i18n.AND : i18n.OR} + + ); + + const getBadgeWithAntennas = () => ( + + + {getBadge()} + + + ); + + return includeAntennas ? getBadgeWithAntennas() : getBadge(); + } +); + +AndOrBadge.displayName = 'AndOrBadge'; diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/translations.ts b/x-pack/plugins/security_solution/public/common/components/and_or_badge/translations.ts new file mode 100644 index 0000000000000..390652738363f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const AND = i18n.translate('xpack.securitySolution.andOrBadge.and', { + defaultMessage: 'AND', +}); + +export const OR = i18n.translate('xpack.securitySolution.andOrBadge.or', { + defaultMessage: 'OR', +}); diff --git a/x-pack/plugins/siem/public/common/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__examples__/index.stories.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/autocomplete_field/__examples__/index.stories.tsx rename to x-pack/plugins/security_solution/public/common/components/autocomplete_field/__examples__/index.stories.tsx diff --git a/x-pack/plugins/siem/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/autocomplete_field/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/autocomplete_field/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/autocomplete_field/index.tsx rename to x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/suggestion_item.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/autocomplete_field/suggestion_item.tsx rename to x-pack/plugins/security_solution/public/common/components/autocomplete_field/suggestion_item.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/__snapshots__/areachart.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/charts/__snapshots__/areachart.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/__snapshots__/areachart.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/charts/__snapshots__/areachart.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/charts/__snapshots__/barchart.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/charts/__snapshots__/barchart.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/__snapshots__/barchart.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/charts/__snapshots__/barchart.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/charts/areachart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/areachart.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/areachart.test.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/areachart.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/areachart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/areachart.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/barchart.test.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/barchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/barchart.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/chart_place_holder.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/chart_place_holder.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/chart_place_holder.test.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/chart_place_holder.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/chart_place_holder.tsx b/x-pack/plugins/security_solution/public/common/components/charts/chart_place_holder.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/chart_place_holder.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/chart_place_holder.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/common.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/common.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/common.test.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/common.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/common.tsx b/x-pack/plugins/security_solution/public/common/components/charts/common.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/common.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/common.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/draggable_legend.test.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/draggable_legend.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/draggable_legend.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.tsx rename to x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/charts/translation.ts b/x-pack/plugins/security_solution/public/common/components/charts/translation.ts new file mode 100644 index 0000000000000..68b90af65aefa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/charts/translation.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALL_VALUES_ZEROS_TITLE = i18n.translate( + 'xpack.securitySolution.chart.dataAllValuesZerosTitle', + { + defaultMessage: 'All values returned zero', + } +); + +export const DATA_NOT_AVAILABLE_TITLE = i18n.translate( + 'xpack.securitySolution.chart.dataNotAvailableTitle', + { + defaultMessage: 'Chart Data Not Available', + } +); + +export const ALL_OTHERS = i18n.translate('xpack.securitySolution.chart.allOthersGroupingLabel', { + defaultMessage: 'All others', +}); diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context.tsx diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.test.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx similarity index 82% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 91ef4f3ac9925..aa5efe3ccfe6a 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -14,10 +14,13 @@ import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { TimelineContext } from '../../../timelines/components/timeline/timeline_context'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; +import { + ManageGlobalTimeline, + timelineDefaults, +} from '../../../timelines/components/manage_timeline'; jest.mock('../../lib/kibana'); @@ -31,8 +34,17 @@ jest.mock('uuid', () => { jest.mock('../../hooks/use_add_to_timeline'); const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const timelineId = 'cool-id'; const field = 'process.name'; const value = 'nice'; +const toggleTopN = jest.fn(); +const defaultProps = { + field, + showTopN: false, + timelineId, + toggleTopN, + value, +}; describe('DraggableWrapperHoverContent', () => { beforeAll(() => { @@ -44,6 +56,9 @@ describe('DraggableWrapperHoverContent', () => { /* eslint-disable no-console */ const originalError = console.error; const originalWarn = console.warn; + beforeEach(() => { + jest.clearAllMocks(); + }); beforeAll(() => { console.warn = jest.fn(); console.error = jest.fn(); @@ -64,12 +79,7 @@ describe('DraggableWrapperHoverContent', () => { test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { const wrapper = mount( - + ); @@ -81,12 +91,7 @@ describe('DraggableWrapperHoverContent', () => { test(`it does NOT render the 'Filter ${hoverAction} value' button when showTopN is true`, () => { const wrapper = mount( - + ); @@ -104,22 +109,22 @@ describe('DraggableWrapperHoverContent', () => { filterManager = new FilterManager(mockUiSettingsForFilterManager); filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); + const manageTimelineForTesting = { + [timelineId]: { + ...timelineDefaults, + id: timelineId, + filterManager, + }, + }; wrapper = mount( - - - + + + ); }); - test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); @@ -157,13 +162,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -204,17 +203,19 @@ describe('DraggableWrapperHoverContent', () => { filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); + const manageTimelineForTesting = { + [timelineId]: { + ...timelineDefaults, + id: timelineId, + filterManager, + }, + }; + wrapper = mount( - - - + + + ); }); @@ -265,13 +266,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -328,11 +323,13 @@ describe('DraggableWrapperHoverContent', () => { @@ -351,11 +348,11 @@ describe('DraggableWrapperHoverContent', () => { @@ -383,10 +380,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -404,10 +401,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -425,10 +422,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -441,16 +438,15 @@ describe('DraggableWrapperHoverContent', () => { }); test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => { - const toggleTopN = jest.fn(); const whitelistedField = 'signal.rule.name'; const wrapper = mount( @@ -471,10 +467,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -494,10 +490,11 @@ describe('DraggableWrapperHoverContent', () => { @@ -515,10 +512,11 @@ describe('DraggableWrapperHoverContent', () => { @@ -537,12 +535,7 @@ describe('DraggableWrapperHoverContent', () => { test(`it renders the 'Copy to Clipboard' button when showTopN is false`, () => { const wrapper = mount( - + ); @@ -553,10 +546,10 @@ describe('DraggableWrapperHoverContent', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx similarity index 87% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index a0546dc64113c..998d18291f638 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -13,17 +13,18 @@ import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; import { createFilter } from '../add_filter_to_global_search_bar'; -import { useTimelineContext } from '../../../timelines/components/timeline/timeline_context'; import { StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; import * as i18n from './translations'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; interface Props { draggableId?: DraggableId; field: string; onFilterAdded?: () => void; showTopN: boolean; + timelineId?: string; toggleTopN: () => void; value?: string[] | string | null; } @@ -33,20 +34,27 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ field, onFilterAdded, showTopN, + timelineId, toggleTopN, value, }) => { const startDragToTimeline = useAddToTimeline({ draggableId, fieldName: field }); const kibana = useKibana(); - const { filterManager: timelineFilterManager } = useTimelineContext(); - const filterManager = useMemo(() => kibana.services.data.query.filterManager, [ + const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); - + const { getTimelineFilterManager } = useManageTimeline(); + const filterManager = useMemo( + () => + timelineId + ? getTimelineFilterManager(timelineId) ?? filterManagerBackup + : filterManagerBackup, + [timelineId, getTimelineFilterManager, filterManagerBackup] + ); const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); - const activeFilterManager = timelineFilterManager ?? filterManager; + const activeFilterManager = filterManager; if (activeFilterManager != null) { activeFilterManager.addFilters(filter); @@ -55,12 +63,12 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ onFilterAdded(); } } - }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + }, [field, value, filterManager, onFilterAdded]); const filterOutValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); - const activeFilterManager = timelineFilterManager ?? filterManager; + const activeFilterManager = filterManager; if (activeFilterManager != null) { activeFilterManager.addFilters(filter); @@ -69,7 +77,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ onFilterAdded(); } } - }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + }, [field, value, filterManager, onFilterAdded]); return ( <> diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.test.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.tsx diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts similarity index 99% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts index be58381fbca1b..afdfde6e08224 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts @@ -960,7 +960,7 @@ describe('helpers', () => { }, ], }, - type: 'x-pack/siem/local/timeline/UPDATE_PROVIDERS', + type: 'x-pack/security_solution/local/timeline/UPDATE_PROVIDERS', }); }); @@ -981,7 +981,7 @@ describe('helpers', () => { payload: { id: 'hosts-table-hostName-ENDPOINT-W-0-01', }, - type: 'x-pack/siem/local/drag_and_drop/NO_PROVIDER_FOUND', + type: 'x-pack/security_solution/local/drag_and_drop/NO_PROVIDER_FOUND', }); }); }); diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/provider_container.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/provider_container.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/drag_and_drop/provider_container.tsx rename to x-pack/plugins/security_solution/public/common/components/drag_and_drop/provider_container.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/translations.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/translations.ts new file mode 100644 index 0000000000000..574b2c190e1db --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/translations.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADD_TO_TIMELINE = i18n.translate('xpack.securitySolution.dragAndDrop.addToTimeline', { + defaultMessage: 'Add to timeline investigation', +}); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'xpack.securitySolution.dragAndDrop.copyToClipboardTooltip', + { + defaultMessage: 'Copy to Clipboard', + } +); + +export const FIELD = i18n.translate('xpack.securitySolution.dragAndDrop.fieldLabel', { + defaultMessage: 'Field', +}); + +export const FILTER_FOR_VALUE = i18n.translate( + 'xpack.securitySolution.dragAndDrop.filterForValueHoverAction', + { + defaultMessage: 'Filter for value', + } +); + +export const FILTER_OUT_VALUE = i18n.translate( + 'xpack.securitySolution.dragAndDrop.filterOutValueHoverAction', + { + defaultMessage: 'Filter out value', + } +); + +export const CLOSE = i18n.translate('xpack.securitySolution.dragAndDrop.closeButtonLabel', { + defaultMessage: 'Close', +}); + +export const SHOW_TOP = (fieldName: string) => + i18n.translate('xpack.securitySolution.overview.showTopTooltip', { + values: { fieldName }, + defaultMessage: `Show top {fieldName}`, + }); diff --git a/x-pack/plugins/siem/public/common/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/draggables/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/draggables/field_badge/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/field_badge/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/draggables/field_badge/index.tsx rename to x-pack/plugins/security_solution/public/common/components/draggables/field_badge/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/field_badge/translations.ts b/x-pack/plugins/security_solution/public/common/components/draggables/field_badge/translations.ts new file mode 100644 index 0000000000000..8d378c9a05a70 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/draggables/field_badge/translations.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CATEGORY = i18n.translate('xpack.securitySolution.draggables.field.categoryLabel', { + defaultMessage: 'Category', +}); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'xpack.securitySolution.eventDetails.copyToClipboardTooltip', + { + defaultMessage: 'Copy to Clipboard', + } +); + +export const FIELD = i18n.translate('xpack.securitySolution.draggables.field.fieldLabel', { + defaultMessage: 'Field', +}); + +export const TYPE = i18n.translate('xpack.securitySolution.draggables.field.typeLabel', { + defaultMessage: 'Type', +}); + +export const VIEW_CATEGORY = i18n.translate( + 'xpack.securitySolution.draggables.field.viewCategoryTooltip', + { + defaultMessage: 'View Category', + } +); diff --git a/x-pack/plugins/siem/public/common/components/draggables/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/draggables/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/draggables/index.tsx rename to x-pack/plugins/security_solution/public/common/components/draggables/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/empty_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/empty_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/empty_page/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/empty_page/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/empty_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/empty_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/empty_page/index.tsx rename to x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/empty_value/empty_value.test.tsx b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/empty_value/empty_value.test.tsx rename to x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/empty_value/index.tsx b/x-pack/plugins/security_solution/public/common/components/empty_value/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/empty_value/index.tsx rename to x-pack/plugins/security_solution/public/common/components/empty_value/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/empty_value/translations.ts b/x-pack/plugins/security_solution/public/common/components/empty_value/translations.ts new file mode 100644 index 0000000000000..afcf21505103d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/empty_value/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const EMPTY_STRING = i18n.translate( + 'xpack.securitySolution.emptyString.emptyStringDescription', + { + defaultMessage: 'Empty String', + } +); diff --git a/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/endpoint/formatted_date_time.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/endpoint/formatted_date_time.tsx rename to x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx diff --git a/x-pack/plugins/siem/public/common/components/endpoint/link_to_app.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/endpoint/link_to_app.test.tsx rename to x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/endpoint/link_to_app.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/endpoint/link_to_app.tsx rename to x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx diff --git a/x-pack/plugins/siem/public/common/components/endpoint/page_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/endpoint/page_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/endpoint/page_view.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx similarity index 97% rename from x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx rename to x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx index 759274e3a4ffa..6fe15310fc88e 100644 --- a/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx @@ -149,7 +149,9 @@ export const PageView = memo( )} )} - {tabs && {tabComponents}} + {tabComponents.length > 0 && ( + {tabComponents} + )} {bodyHeader && ( diff --git a/x-pack/plugins/siem/public/common/components/endpoint/route_capture.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/endpoint/route_capture.tsx rename to x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx diff --git a/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.tsx b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.tsx rename to x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap rename to x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx similarity index 99% rename from x-pack/plugins/siem/public/common/components/event_details/columns.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 7b6e9fb21a3e3..e01ccf1e544bb 100644 --- a/x-pack/plugins/siem/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -147,6 +147,7 @@ export const getColumns = ({ data-test-subj="field-name" fieldId={field} onUpdateColumns={onUpdateColumns} + timelineId={contextId} />
)} diff --git a/x-pack/plugins/siem/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/event_details.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/event_details.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/event_id.ts b/x-pack/plugins/security_solution/public/common/components/event_details/event_id.ts similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/event_id.ts rename to x-pack/plugins/security_solution/public/common/components/event_details/event_id.ts diff --git a/x-pack/plugins/siem/public/common/components/event_details/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/helpers.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/helpers.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/json_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/json_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/json_view.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/stateful_event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/stateful_event_details.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts new file mode 100644 index 0000000000000..19e71e0f37da6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', { + defaultMessage: 'Table', +}); + +export const JSON_VIEW = i18n.translate('xpack.securitySolution.eventDetails.jsonView', { + defaultMessage: 'JSON View', +}); + +export const FIELD = i18n.translate('xpack.securitySolution.eventDetails.field', { + defaultMessage: 'Field', +}); + +export const VALUE = i18n.translate('xpack.securitySolution.eventDetails.value', { + defaultMessage: 'Value', +}); + +export const DESCRIPTION = i18n.translate('xpack.securitySolution.eventDetails.description', { + defaultMessage: 'Description', +}); + +export const BLANK = i18n.translate('xpack.securitySolution.eventDetails.blank', { + defaultMessage: ' ', +}); + +export const PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.eventDetails.filter.placeholder', + { + defaultMessage: 'Filter by Field, Value, or Description...', + } +); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'xpack.securitySolution.eventDetails.copyToClipboard', + { + defaultMessage: 'Copy to Clipboard', + } +); + +export const TOGGLE_COLUMN_TOOLTIP = i18n.translate( + 'xpack.securitySolution.eventDetails.toggleColumnTooltip', + { + defaultMessage: 'Toggle column', + } +); diff --git a/x-pack/plugins/siem/public/common/components/event_details/types.ts b/x-pack/plugins/security_solution/public/common/components/event_details/types.ts similarity index 100% rename from x-pack/plugins/siem/public/common/components/event_details/types.ts rename to x-pack/plugins/security_solution/public/common/components/event_details/types.ts diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/default_headers.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/events_viewer/default_headers.tsx rename to x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/default_model.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/events_viewer/default_model.tsx rename to x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/event_details_width_context.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_width_context.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/events_viewer/event_details_width_context.tsx rename to x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_width_context.tsx diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.test.tsx rename to x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx new file mode 100644 index 0000000000000..d0bd87188e541 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -0,0 +1,249 @@ +/* + * 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 { EuiPanel } from '@elastic/eui'; +import { getOr, isEmpty, union } from 'lodash/fp'; +import React, { useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { BrowserFields } from '../../containers/source'; +import { TimelineQuery } from '../../../timelines/containers'; +import { Direction } from '../../../graphql/types'; +import { useKibana } from '../../lib/kibana'; +import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; +import { HeaderSection } from '../header_section'; +import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; +import { Sort } from '../../../timelines/components/timeline/body/sort'; +import { StatefulBody } from '../../../timelines/components/timeline/body/stateful_body'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events'; +import { Footer, footerHeight } from '../../../timelines/components/timeline/footer'; +import { combineQueries } from '../../../timelines/components/timeline/helpers'; +import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline'; +import { EventDetailsWidthProvider } from './event_details_width_context'; +import * as i18n from './translations'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../store'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; + +const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; + +const StyledEuiPanel = styled(EuiPanel)` + max-width: 100%; +`; + +const EventsContainerLoading = styled.div` + width: 100%; + overflow: auto; +`; + +interface Props { + browserFields: BrowserFields; + columns: ColumnHeaderOptions[]; + dataProviders: DataProvider[]; + deletedEventIds: Readonly; + end: number; + filters: Filter[]; + headerFilterGroup?: React.ReactNode; + height?: number; + id: string; + indexPattern: IIndexPattern; + isLive: boolean; + itemsPerPage: number; + itemsPerPageOptions: number[]; + kqlMode: KqlMode; + onChangeItemsPerPage: OnChangeItemsPerPage; + query: Query; + start: number; + sort: Sort; + toggleColumn: (column: ColumnHeaderOptions) => void; + utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; +} + +const EventsViewerComponent: React.FC = ({ + browserFields, + columns, + dataProviders, + deletedEventIds, + end, + filters, + headerFilterGroup, + height = DEFAULT_EVENTS_VIEWER_HEIGHT, + id, + indexPattern, + isLive, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + onChangeItemsPerPage, + query, + start, + sort, + toggleColumn, + utilityBar, +}) => { + const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const kibana = useKibana(); + const { filterManager } = useKibana().services.data.query; + const [isQueryLoading, setIsQueryLoading] = useState(false); + + const { + getManageTimelineById, + setIsTimelineLoading, + setTimelineFilterManager, + } = useManageTimeline(); + useEffect(() => { + setIsTimelineLoading({ id, isLoading: isQueryLoading }); + }, [isQueryLoading]); + useEffect(() => { + setTimelineFilterManager({ id, filterManager }); + }, [filterManager]); + + const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [ + getManageTimelineById, + id, + ]); + + const combinedQueries = combineQueries({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders, + indexPattern, + browserFields, + filters, + kqlQuery: query, + kqlMode, + start, + end, + isEventViewer: true, + }); + const fields = useMemo( + () => + union( + columnsHeader.map((c) => c.id), + queryFields ?? [] + ), + [columnsHeader, queryFields] + ); + const sortField = useMemo( + () => ({ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }), + [sort.columnId, sort.sortDirection] + ); + + return ( + + {combinedQueries != null ? ( + + + {({ + events, + getUpdatedAt, + inspect, + loading, + loadMore, + pageInfo, + refetch, + totalCount = 0, + }) => { + setIsQueryLoading(loading); + const totalCountMinusDeleted = + totalCount > 0 ? totalCount - deletedEventIds.length : 0; + + const subtitle = `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${unit( + totalCountMinusDeleted + )}`; + + return ( + <> + + {headerFilterGroup} + + + {utilityBar?.(refetch, totalCountMinusDeleted)} + + + + + !deletedEventIds.includes(e._id))} + id={id} + isEventViewer={true} + height={height} + sort={sort} + toggleColumn={toggleColumn} + /> + +