diff --git a/.ci/Dockerfile b/.ci/Dockerfile new file mode 100644 index 000000000000..d90d9f4710b5 --- /dev/null +++ b/.ci/Dockerfile @@ -0,0 +1,38 @@ +# NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. +# If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts + +ARG NODE_VERSION=10.21.0 + +FROM node:${NODE_VERSION} AS base + +RUN apt-get update && \ + apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ + libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ + libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ + libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget openjdk-8-jre && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ + && apt-get update \ + && apt-get install -y rsync jq bsdtar google-chrome-stable \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN LATEST_VAULT_RELEASE=$(curl -s https://api.github.com/repos/hashicorp/vault/tags | jq --raw-output .[0].name[1:]) \ + && curl -L https://releases.hashicorp.com/vault/${LATEST_VAULT_RELEASE}/vault_${LATEST_VAULT_RELEASE}_linux_amd64.zip -o vault.zip \ + && unzip vault.zip \ + && rm vault.zip \ + && chmod +x vault \ + && mv vault /usr/local/bin/vault + +RUN groupadd -r kibana && useradd -r -g kibana kibana && mkdir /home/kibana && chown kibana:kibana /home/kibana + +COPY ./bash_standard_lib.sh /usr/local/bin/bash_standard_lib.sh +RUN chmod +x /usr/local/bin/bash_standard_lib.sh + +COPY ./runbld /usr/local/bin/runbld +RUN chmod +x /usr/local/bin/runbld + +USER kibana diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index b0d359182164..9a49c19b94df 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -7,18 +7,22 @@ kibanaPipeline(timeoutMinutes: 120) { githubCommitStatus.trackBuild(params.commit, 'kibana-ci-baseline') { ciStats.trackBuild { catchError { - parallel([ - 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) - } - }, - 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) - } - }, - ]) + withEnv([ + 'CI_PARALLEL_PROCESS_NUMBER=1' + ]) { + parallel([ + 'oss-visualRegression': { + workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')() + } + }, + 'xpack-visualRegression': { + workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')() + } + }, + ]) + } } kibanaPipeline.sendMail() diff --git a/.ci/runbld_no_junit.yml b/.ci/runbld_no_junit.yml index 67b5002c1c43..1bcb7e22a264 100644 --- a/.ci/runbld_no_junit.yml +++ b/.ci/runbld_no_junit.yml @@ -3,4 +3,4 @@ profiles: - ".*": # Match any job tests: - junit-filename-pattern: "8d8bd494-d909-4e67-a052-7e8b5aaeb5e4" # A bogus path that should never exist + junit-filename-pattern: false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f1a374445657..73fb10532fd8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,7 +7,6 @@ /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app diff --git a/.gitignore b/.gitignore index dfd02de7b118..1d12ef2a9cff 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ npm-debug.log* .tern-project .nyc_output .ci/pipeline-library/build/ +.ci/runbld +.ci/bash_standard_lib.sh .gradle # apm plugin diff --git a/Jenkinsfile b/Jenkinsfile index ad1d244c7887..3b68cde20657 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,49 +9,7 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) ciStats.trackBuild { catchError { retryable.enable() - parallel([ - 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), - 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), - 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), - 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), - 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), - 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), - 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), - 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), - 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), - 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), - 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), - 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), - 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), - 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), - // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), - ]), - 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), - 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), - 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), - 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), - 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), - 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), - 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), - 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), - 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), - 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), - 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), - 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), - 'xpack-securitySolutionCypress': { processNumber -> - whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { - kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) - } - }, - - // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), - ]), - ]) + kibanaPipeline.allCiTasks() } } } diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc index 4481dea44795..bb7222020180 100644 --- a/docs/developer/architecture/code-exploration.asciidoc +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -58,9 +58,9 @@ The Charts plugin is a way to create easier integration of shared colors, themes WARNING: Missing README. -- {kib-repo}blob/{branch}/src/plugins/dashboard[dashboard] +- {kib-repo}blob/{branch}/src/plugins/dashboard/README.md[dashboard] -WARNING: Missing README. +Contains the dashboard application. - {kib-repo}blob/{branch}/src/plugins/data/README.md[data] @@ -76,9 +76,9 @@ Routing will be handled by the id of the dev tool - your dev tool will be mounte This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. -- {kib-repo}blob/{branch}/src/plugins/discover[discover] +- {kib-repo}blob/{branch}/src/plugins/discover/README.md[discover] -WARNING: Missing README. +Contains the Discover application and the saved search embeddable. - {kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] @@ -109,9 +109,9 @@ Moves the legacy ui/registry/feature_catalogue module for registering "features" WARNING: Missing README. -- {kib-repo}blob/{branch}/src/plugins/input_control_vis[inputControlVis] +- {kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] -WARNING: Missing README. +Contains the input control visualization allowing to place custom filter controls on a dashboard. - {kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] @@ -206,9 +206,10 @@ This plugin adds the Advanced Settings section for the Usage Data collection (ak WARNING: Missing README. -- {kib-repo}blob/{branch}/src/plugins/timelion[timelion] +- {kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] -WARNING: Missing README. +Contains the deprecated timelion application. For the timelion visualization, +which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. - {kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] @@ -222,59 +223,63 @@ Usage Collection allows collecting usage data for other services to consume (tel To integrate with the telemetry services for usage collection of your feature, there are 2 steps: -- {kib-repo}blob/{branch}/src/plugins/vis_type_markdown[visTypeMarkdown] +- {kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown] -WARNING: Missing README. +The markdown visualization that can be used to place text panels on dashboards. -- {kib-repo}blob/{branch}/src/plugins/vis_type_metric[visTypeMetric] +- {kib-repo}blob/{branch}/src/plugins/vis_type_metric/README.md[visTypeMetric] -WARNING: Missing README. +Contains the metric visualization. -- {kib-repo}blob/{branch}/src/plugins/vis_type_table[visTypeTable] +- {kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable] -WARNING: Missing README. +Contains the data table visualization, that allows presenting data in a simple table format. -- {kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud[visTypeTagcloud] +- {kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud/README.md[visTypeTagcloud] -WARNING: Missing README. +Contains the tagcloud visualization. - {kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] -If your grammar was changed in public/chain.peg you need to re-generate the static parser. You could use a grunt task: +Contains the timelion visualization and the timelion backend. -- {kib-repo}blob/{branch}/src/plugins/vis_type_timeseries[visTypeTimeseries] +- {kib-repo}blob/{branch}/src/plugins/vis_type_timeseries/README.md[visTypeTimeseries] -WARNING: Missing README. +Contains everything around TSVB (the editor, visualizatin implementations and backends). -- {kib-repo}blob/{branch}/src/plugins/vis_type_vega[visTypeVega] +- {kib-repo}blob/{branch}/src/plugins/vis_type_vega/README.md[visTypeVega] -WARNING: Missing README. +Contains the Vega visualization. -- {kib-repo}blob/{branch}/src/plugins/vis_type_vislib[visTypeVislib] +- {kib-repo}blob/{branch}/src/plugins/vis_type_vislib/README.md[visTypeVislib] -WARNING: Missing README. +Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and +heatmap charts. -- {kib-repo}blob/{branch}/src/plugins/vis_type_xy[visTypeXy] +- {kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] -WARNING: Missing README. +Contains the new xy-axis chart using the elastic-charts library, which will eventually +replace the vislib xy-axis (bar, area, line) charts. -- {kib-repo}blob/{branch}/src/plugins/visualizations[visualizations] +- {kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] -WARNING: Missing README. +Contains most of the visualization infrastructure, e.g. the visualization type registry or the +visualization embeddable. -- {kib-repo}blob/{branch}/src/plugins/visualize[visualize] +- {kib-repo}blob/{branch}/src/plugins/visualize/README.md[visualize] -WARNING: Missing README. +Contains the visualize application which includes the listing page and the app frame, +which will load the visualization's editor. [discrete] @@ -345,9 +350,12 @@ You can run a local cluster and simulate a remote cluster within a single Kibana - {kib-repo}blob/{branch}/x-pack/plugins/dashboard_enhanced/README.md[dashboardEnhanced] -- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode[dashboardMode] +Contains the enhancements to the OSS dashboard app. -WARNING: Missing README. + +- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode/README.md[dashboardMode] + +The deprecated dashboard only mode. - {kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] @@ -355,9 +363,9 @@ WARNING: Missing README. WARNING: Missing README. -- {kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced[discoverEnhanced] +- {kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] -WARNING: Missing README. +Contains the enhancements to the OSS discover app. - {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced[embeddableEnhanced] diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index 4d2fac028703..30e980b5ffc5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -35,7 +35,7 @@ export declare class SearchInterceptor | Method | Modifiers | Description | | --- | --- | --- | -| [runSearch(request, combinedSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | +| [runSearch(request, signal)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates the pendingCount when the request is started/finalized. | | [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md index 385d4f6a238d..3601a00c48cf 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md @@ -7,7 +7,7 @@ Signature: ```typescript -protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable; +protected runSearch(request: IEsSearchRequest, signal: AbortSignal): Observable; ``` ## Parameters @@ -15,7 +15,7 @@ protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Obs | Parameter | Type | Description | | --- | --- | --- | | request | IEsSearchRequest | | -| combinedSignal | AbortSignal | | +| signal | AbortSignal | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md index 85abd9d9dba9..1a94a709cc21 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md @@ -9,5 +9,5 @@ Used internally for telemetry Signature: ```typescript -usage: SearchUsage; +usage?: SearchUsage; ``` 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 6bf481841f33..1bcd575803f8 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 @@ -27,6 +27,7 @@ | [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally | | [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | | +| [usageProvider(core)](./kibana-plugin-plugins-data-server.usageprovider.md) | | ## Interfaces @@ -49,6 +50,7 @@ | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | | [Query](./kibana-plugin-plugins-data-server.query.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-server.refreshinterval.md) | | +| [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) | | | [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | ## Variables diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md new file mode 100644 index 000000000000..d867509e915b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) + +## SearchUsage interface + +Signature: + +```typescript +export interface SearchUsage +``` + +## Methods + +| Method | Description | +| --- | --- | +| [trackError()](./kibana-plugin-plugins-data-server.searchusage.trackerror.md) | | +| [trackSuccess(duration)](./kibana-plugin-plugins-data-server.searchusage.tracksuccess.md) | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md new file mode 100644 index 000000000000..212133588f62 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) > [trackError](./kibana-plugin-plugins-data-server.searchusage.trackerror.md) + +## SearchUsage.trackError() method + +Signature: + +```typescript +trackError(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md new file mode 100644 index 000000000000..b58f440c7dcc --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) > [trackSuccess](./kibana-plugin-plugins-data-server.searchusage.tracksuccess.md) + +## SearchUsage.trackSuccess() method + +Signature: + +```typescript +trackSuccess(duration: number): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| duration | number | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md new file mode 100644 index 000000000000..ad5c61b5c85a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [usageProvider](./kibana-plugin-plugins-data-server.usageprovider.md) + +## usageProvider() function + +Signature: + +```typescript +export declare function usageProvider(core: CoreSetup): SearchUsage; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | + +Returns: + +`SearchUsage` + diff --git a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc b/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc deleted file mode 100644 index 0097bf8c648f..000000000000 --- a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc +++ /dev/null @@ -1,179 +0,0 @@ -[role="xpack"] - -[[example-using-index-lifecycle-policy]] -=== Tutorial: Use {ilm-init} to manage {filebeat} time-based indices - -With {ilm} ({ilm-init}), you can create policies that perform actions automatically -on indices as they age and grow. {ilm-init} policies help you to manage -performance, resilience, and retention of your data during its lifecycle. This tutorial shows -you how to use {kib}’s *Index Lifecycle Policies* to modify and create {ilm-init} -policies. You can learn more about all of the actions, benefits, and lifecycle -phases in the {ref}/overview-index-lifecycle-management.html[{ilm-init} overview]. - - -[discrete] -[[example-using-index-lifecycle-policy-scenario]] -==== Scenario - -You’re tasked with sending syslog files to an {es} cluster. This -log data has the following data retention guidelines: - -* Keep logs on hot data nodes for 30 days -* Roll over to a new index if the size reaches 50GB -* After 30 days: -** Move the logs to warm data nodes -** Set {ref}/glossary.html#glossary-replica-shard[replica shards] to 1 -** {ref}/indices-forcemerge.html[Force merge] multiple index segments to free up the space used by deleted documents -* Delete logs after 90 days - - -[discrete] -[[example-using-index-lifecycle-policy-prerequisites]] -==== Prerequisites - -To complete this tutorial, you'll need: - -* An {es} cluster with hot and warm nodes configured for shard allocation -awareness. If you’re using {cloud}/ec-getting-started-templates-hot-warm.html[{ess}], -choose the hot-warm architecture deployment template. - -+ -For a self-managed cluster, add node attributes as described for {ref}/shard-allocation-filtering.html[shard allocation filtering] -to label data nodes as hot or warm. This step is required to migrate shards between -nodes configured with specific hardware for the hot or warm phases. -+ -For example, you can set this in your `elasticsearch.yml` for each data node: -+ -[source,yaml] --------------------------------------------------------------------------------- -node.attr.data: "warm" --------------------------------------------------------------------------------- - -* A server with {filebeat} installed and configured to send logs to the `elasticsearch` -output as described in {filebeat-ref}/filebeat-getting-started.html[Getting Started with {filebeat}]. - -[discrete] -[[example-using-index-lifecycle-policy-view-fb-ilm-policy]] -==== View the {filebeat} {ilm-init} policy - -{filebeat} includes a default {ilm-init} policy that enables rollover. {ilm-init} -is enabled automatically if you’re using the default `filebeat.yml` and index template. - -To view the default policy in {kib}, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*, -search for _filebeat_, and choose the _filebeat-version_ policy. - -This policy initiates the rollover action when the index size reaches 50GB or -becomes 30 days old. - -[role="screenshot"] -image::images/tutorial-ilm-hotphaserollover-default.png["Default policy"] - - -[float] -==== Modify the policy - -The default policy is enough to prevent the creation of many tiny daily indices. -You can modify the policy to meet more complex requirements. - -. Activate the warm phase. - -+ -. Set either of the following options to control when the index moves to the warm phase: - -** Provide a value for *Timing for warm phase*. Setting this to *15* keeps the -indices on hot nodes for a range of 15-45 days, depending on when the initial -rollover occurred. - -** Enable *Move to warm phase on rollover*. The index might move to the warm phase -more quickly than intended if it reaches the *Maximum index size* before the -the *Maximum age*. - -. In the *Select a node attribute to control shard allocation* dropdown, select -*data:warm(2)* to migrate shards to warm data nodes. - -. Change *Number of replicas* to *1*. - -. Enable *Force merge data* and set *Number of segments* to *1*. -+ -NOTE: When rollover is enabled in the hot phase, action timing in the other phases -is based on the rollover date. - -+ -[role="screenshot"] -image::images/tutorial-ilm-modify-default-warm-phase-rollover.png["Modify to add warm phase"] - -. Activate the delete phase and set *Timing for delete phase* to *90* days. -+ -[role="screenshot"] -image::images/tutorial-ilm-delete-rollover.png["Add a delete phase"] - -[float] -==== Create a custom policy - -If meeting a specific retention time period is most important, you can create a -custom policy. For this option, you will use {filebeat} daily indices without -rollover. - -. To create a custom policy, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*, then click -*Create policy*. - -. Activate the warm phase and configure it as follows: -+ -|=== -|*Setting* |*Value* - -|Timing for warm phase -|30 days from index creation - -|Node attribute -|`data:warm` - -|Number of replicas -|1 - -|Force merge data -|enable - -|Number of segments -|1 -|=== - -+ -[role="screenshot"] -image::images/tutorial-ilm-custom-policy.png["Modify the custom policy to add a warm phase"] - - -+ -. Activate the delete phase and set the timing. -+ -|=== -|*Setting* |*Value* -|Timing for delete phase -|90 -|=== - -+ -[role="screenshot"] -image::images/tutorial-ilm-delete-phase-creation.png["Delete phase"] - -. To configure the index to use the new policy, open the menu, then go to *Stack Management > Data > Index Lifecycle -Policies*. - -.. Find your {ilm-init} policy. -.. Click the *Actions* link next to your policy name. -.. Choose *Add policy to index template*. -.. Select your {filebeat} index template name from the *Index template* list. For example, `filebeat-7.5.x`. -.. Click *Add Policy* to save the changes. - -+ -NOTE: If you initially used the default {filebeat} {ilm-init} policy, you will -see a notice that the template already has a policy associated with it. Confirm -that you want to overwrite that configuration. - -+ -+ -TIP: When you change the policy associated with the index template, the active -index will continue to use the policy it was associated with at index creation -unless you manually update it. The next new index will use the updated policy. -For more reasons that your {ilm-init} policy changes might be delayed, see -{ref}/update-lifecycle-policy.html#update-lifecycle-policy[Update Lifecycle Policy]. diff --git a/docs/observability/images/observability-overview.png b/docs/observability/images/observability-overview.png new file mode 100644 index 000000000000..b7d3d09139a8 Binary files /dev/null and b/docs/observability/images/observability-overview.png differ diff --git a/docs/observability/index.asciidoc b/docs/observability/index.asciidoc new file mode 100644 index 000000000000..d63402e8df2f --- /dev/null +++ b/docs/observability/index.asciidoc @@ -0,0 +1,24 @@ +[chapter] +[role="xpack"] +[[observability]] += Observability + +Observability enables you to add and monitor your logs, system +metrics, uptime data, and application traces, as a single stack. + +With *Observability*, you have: + +* A central place to add and configure your data sources. +* A variety of charts displaying analytics relating to each data source. +* *View in app* options to drill down and analyze data in the Logs, Metrics, Uptime, and APM apps. +* An alerts chart to keep you informed of any issues that you may need to resolve quickly. + +[role="screenshot"] +image::observability/images/observability-overview.png[Observability Overview in {kib}] + +[float] +== Get started + +{kib} provides step-by-step instructions to help you add and configure your data +sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information +and instructions. diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 01be8c2e264c..abbdbeb68d9c 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -27,6 +27,8 @@ include::graph/index.asciidoc[] include::visualize.asciidoc[] +include::{kib-repo-dir}/observability/index.asciidoc[] + include::{kib-repo-dir}/logs/index.asciidoc[] include::{kib-repo-dir}/infrastructure/index.asciidoc[] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 1704a8084765..bc96463f6efb 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -190,8 +190,6 @@ include::{kib-repo-dir}/management/index-lifecycle-policies/manage-policy.asciid include::{kib-repo-dir}/management/index-lifecycle-policies/add-policy-to-index.asciidoc[] -include::{kib-repo-dir}/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc[] - include::{kib-repo-dir}/management/managing-indices.asciidoc[] include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] diff --git a/packages/kbn-dev-utils/src/run/help.test.ts b/packages/kbn-dev-utils/src/run/help.test.ts index 27be7ad28b81..300f1cba7eb7 100644 --- a/packages/kbn-dev-utils/src/run/help.test.ts +++ b/packages/kbn-dev-utils/src/run/help.test.ts @@ -57,7 +57,7 @@ const barCommand: Command = { usage: 'bar [...names]', }; -describe('getHelp()', () => { +describe.skip('getHelp()', () => { it('returns the expected output', () => { expect( getHelp({ @@ -95,7 +95,7 @@ describe('getHelp()', () => { }); }); -describe('getCommandLevelHelp()', () => { +describe.skip('getCommandLevelHelp()', () => { it('returns the expected output', () => { expect( getCommandLevelHelp({ @@ -141,7 +141,7 @@ describe('getCommandLevelHelp()', () => { }); }); -describe('getHelpForAllCommands()', () => { +describe.skip('getHelpForAllCommands()', () => { it('returns the expected output', () => { expect( getHelpForAllCommands({ diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts index 884614c8b955..4008cf852c3a 100644 --- a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts +++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts @@ -19,9 +19,12 @@ import { REPO_ROOT } from '../repo_root'; -export function createAbsolutePathSerializer(rootPath: string = REPO_ROOT) { +export function createAbsolutePathSerializer( + rootPath: string = REPO_ROOT, + replacement = '' +) { return { test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), - serialize: (value: string) => value.replace(rootPath, '').replace(/\\/g, '/'), + serialize: (value: string) => value.replace(rootPath, replacement).replace(/\\/g, '/'), }; } diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 4fbbc920c444..e6eb5de31abd 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -15,12 +15,9 @@ "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", "@types/compression-webpack-plugin": "^2.0.2", - "@types/estree": "^0.0.44", "@types/loader-utils": "^1.1.3", "@types/watchpack": "^1.1.5", "@types/webpack": "^4.41.3", - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts index 7607e270b5b4..578108fce51f 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -104,4 +104,18 @@ export class BundleCache { public getOptimizerCacheKey() { return this.get().optimizerCacheKey; } + + public clear() { + this.state = undefined; + + if (this.path) { + try { + Fs.unlinkSync(this.path); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } + } } diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts deleted file mode 100644 index aba4451622dc..000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts +++ /dev/null @@ -1,194 +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 estree from 'estree'; - -export interface DisallowedSyntaxCheck { - name: string; - nodeType: estree.Node['type'] | Array; - test?: (n: any) => boolean | void; -} - -export const checks: DisallowedSyntaxCheck[] = [ - /** - * es2015 - */ - // https://github.com/estree/estree/blob/master/es2015.md#functions - { - name: '[es2015] generator function', - nodeType: ['FunctionDeclaration', 'FunctionExpression'], - test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => !!n.generator, - }, - // https://github.com/estree/estree/blob/master/es2015.md#forofstatement - { - name: '[es2015] for-of statement', - nodeType: 'ForOfStatement', - }, - // https://github.com/estree/estree/blob/master/es2015.md#variabledeclaration - { - name: '[es2015] let/const variable declaration', - nodeType: 'VariableDeclaration', - test: (n: estree.VariableDeclaration) => n.kind === 'let' || n.kind === 'const', - }, - // https://github.com/estree/estree/blob/master/es2015.md#expressions - { - name: '[es2015] `super`', - nodeType: 'Super', - }, - // https://github.com/estree/estree/blob/master/es2015.md#expressions - { - name: '[es2015] ...spread', - nodeType: 'SpreadElement', - }, - // https://github.com/estree/estree/blob/master/es2015.md#arrowfunctionexpression - { - name: '[es2015] arrow function expression', - nodeType: 'ArrowFunctionExpression', - }, - // https://github.com/estree/estree/blob/master/es2015.md#yieldexpression - { - name: '[es2015] `yield` expression', - nodeType: 'YieldExpression', - }, - // https://github.com/estree/estree/blob/master/es2015.md#templateliteral - { - name: '[es2015] template literal', - nodeType: 'TemplateLiteral', - }, - // https://github.com/estree/estree/blob/master/es2015.md#patterns - { - name: '[es2015] destructuring', - nodeType: ['ObjectPattern', 'ArrayPattern', 'AssignmentPattern'], - }, - // https://github.com/estree/estree/blob/master/es2015.md#classes - { - name: '[es2015] class', - nodeType: [ - 'ClassDeclaration', - 'ClassExpression', - 'ClassBody', - 'MethodDefinition', - 'MetaProperty', - ], - }, - - /** - * es2016 - */ - { - name: '[es2016] exponent operator', - nodeType: 'BinaryExpression', - test: (n: estree.BinaryExpression) => n.operator === '**', - }, - { - name: '[es2016] exponent assignment', - nodeType: 'AssignmentExpression', - test: (n: estree.AssignmentExpression) => n.operator === '**=', - }, - - /** - * es2017 - */ - // https://github.com/estree/estree/blob/master/es2017.md#function - { - name: '[es2017] async function', - nodeType: ['FunctionDeclaration', 'FunctionExpression'], - test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => n.async, - }, - // https://github.com/estree/estree/blob/master/es2017.md#awaitexpression - { - name: '[es2017] await expression', - nodeType: 'AwaitExpression', - }, - - /** - * es2018 - */ - // https://github.com/estree/estree/blob/master/es2018.md#statements - { - name: '[es2018] for-await-of statements', - nodeType: 'ForOfStatement', - test: (n: estree.ForOfStatement) => n.await, - }, - // https://github.com/estree/estree/blob/master/es2018.md#expressions - { - name: '[es2018] object spread properties', - nodeType: 'ObjectExpression', - test: (n: estree.ObjectExpression) => n.properties.some((p) => p.type === 'SpreadElement'), - }, - // https://github.com/estree/estree/blob/master/es2018.md#template-literals - { - name: '[es2018] tagged template literal with invalid escape', - nodeType: 'TemplateElement', - test: (n: estree.TemplateElement) => n.value.cooked === null, - }, - // https://github.com/estree/estree/blob/master/es2018.md#patterns - { - name: '[es2018] rest properties', - nodeType: 'ObjectPattern', - test: (n: estree.ObjectPattern) => n.properties.some((p) => p.type === 'RestElement'), - }, - - /** - * es2019 - */ - // https://github.com/estree/estree/blob/master/es2019.md#catchclause - { - name: '[es2019] catch clause without a binding', - nodeType: 'CatchClause', - test: (n: estree.CatchClause) => !n.param, - }, - - /** - * es2020 - */ - // https://github.com/estree/estree/blob/master/es2020.md#bigintliteral - { - name: '[es2020] bigint literal', - nodeType: 'Literal', - test: (n: estree.Literal) => typeof n.value === 'bigint', - }, - - /** - * webpack transforms import/export in order to support tree shaking and async imports - * - * // https://github.com/estree/estree/blob/master/es2020.md#importexpression - * { - * name: '[es2020] import expression', - * nodeType: 'ImportExpression', - * }, - * // https://github.com/estree/estree/blob/master/es2020.md#exportalldeclaration - * { - * name: '[es2020] export all declaration', - * nodeType: 'ExportAllDeclaration', - * }, - * - */ -]; - -export const checksByNodeType = new Map(); -for (const check of checks) { - const nodeTypes = Array.isArray(check.nodeType) ? check.nodeType : [check.nodeType]; - for (const nodeType of nodeTypes) { - if (!checksByNodeType.has(nodeType)) { - checksByNodeType.set(nodeType, []); - } - checksByNodeType.get(nodeType)!.push(check); - } -} diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts deleted file mode 100644 index 8fb7559f3e22..000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts +++ /dev/null @@ -1,73 +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 webpack from 'webpack'; -import acorn from 'acorn'; -import * as AcornWalk from 'acorn-walk'; - -import { checksByNodeType, DisallowedSyntaxCheck } from './disallowed_syntax'; -import { parseFilePath } from '../parse_path'; - -export class DisallowedSyntaxPlugin { - apply(compiler: webpack.Compiler) { - compiler.hooks.normalModuleFactory.tap(DisallowedSyntaxPlugin.name, (factory) => { - factory.hooks.parser.for('javascript/auto').tap(DisallowedSyntaxPlugin.name, (parser) => { - parser.hooks.program.tap(DisallowedSyntaxPlugin.name, (program: acorn.Node) => { - const module = parser.state?.current; - if (!module || !module.resource) { - return; - } - - const resource: string = module.resource; - const { dirs } = parseFilePath(resource); - - if (!dirs.includes('node_modules')) { - return; - } - - const failedChecks = new Set(); - - AcornWalk.full(program, (node) => { - const checks = checksByNodeType.get(node.type as any); - if (!checks) { - return; - } - - for (const check of checks) { - if (!check.test || check.test(node)) { - failedChecks.add(check); - } - } - }); - - if (!failedChecks.size) { - return; - } - - // throw an error to trigger a parse failure, causing this module to be reported as invalid - throw new Error( - `disallowed syntax found in file ${resource}:\n - ${Array.from(failedChecks) - .map((c) => c.name) - .join('\n - ')}` - ); - }); - }); - }); - } -} diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index 89cde2c1cd06..5f17a9b38f9f 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -27,6 +27,5 @@ export * from './ts_helpers'; export * from './rxjs_helpers'; export * from './array_helpers'; export * from './event_stream_helpers'; -export * from './disallowed_syntax_plugin'; export * from './parse_path'; export * from './theme_tags'; diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 29922944e881..39cf2120baf0 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -20,5 +20,4 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; -export * from './common/disallowed_syntax_plugin'; export * from './report_optimizer_stats'; diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index a823f66cf767..702ad16144e7 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -21,7 +21,9 @@ import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { getPluginBundles } from './get_plugin_bundles'; -expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo', '')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/output', '')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/outside/of/repo', '')); it('returns a bundle for core and each plugin', () => { expect( @@ -56,46 +58,47 @@ it('returns a bundle for core and each plugin', () => { manifestPath: '/repo/x-pack/plugins/box/kibana.json', }, ], - '/repo' + '/repo', + '/output' ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ Object { "banner": undefined, - "contextDir": /plugins/foo, + "contextDir": /plugins/foo, "id": "foo", - "manifestPath": /plugins/foo/kibana.json, - "outputDir": /plugins/foo/target/public, + "manifestPath": /plugins/foo/kibana.json, + "outputDir": /plugins/foo/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, Object { "banner": undefined, - "contextDir": "/outside/of/repo/plugins/baz", + "contextDir": /plugins/baz, "id": "baz", - "manifestPath": "/outside/of/repo/plugins/baz/kibana.json", - "outputDir": "/outside/of/repo/plugins/baz/target/public", + "manifestPath": /plugins/baz/kibana.json, + "outputDir": /plugins/baz/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, Object { "banner": "/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ ", - "contextDir": /x-pack/plugins/box, + "contextDir": /x-pack/plugins/box, "id": "box", - "manifestPath": /x-pack/plugins/box/kibana.json, - "outputDir": /x-pack/plugins/box/target/public, + "manifestPath": /x-pack/plugins/box/kibana.json, + "outputDir": /x-pack/plugins/box/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, ] diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 9350b9464242..d2d19dcd87cc 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -23,7 +23,11 @@ import { Bundle } from '../common'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; -export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { +export function getPluginBundles( + plugins: KibanaPlatformPlugin[], + repoRoot: string, + outputRoot: string +) { const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; return plugins @@ -36,7 +40,11 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri publicDirNames: ['public', ...p.extraPublicDirs], sourceRoot: repoRoot, contextDir: p.directory, - outputDir: Path.resolve(p.directory, 'target/public'), + outputDir: Path.resolve( + outputRoot, + Path.relative(repoRoot, p.directory), + 'target/public' + ), manifestPath: p.manifestPath, banner: p.directory.startsWith(xpackDirSlash) ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.\n` + diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index f97646e2bbbd..afc2dc8952c8 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -23,16 +23,20 @@ jest.mock('./get_plugin_bundles.ts'); jest.mock('../common/theme_tags.ts'); jest.mock('./filter_by_id.ts'); -import Path from 'path'; -import Os from 'os'; +jest.mock('os', () => { + const realOs = jest.requireActual('os'); + jest.spyOn(realOs, 'cpus').mockImplementation(() => { + return ['foo'] as any; + }); + return realOs; +}); +import Path from 'path'; import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; -import { OptimizerConfig } from './optimizer_config'; +import { OptimizerConfig, ParsedOptions } from './optimizer_config'; import { parseThemeTags } from '../common'; -jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any); - expect.addSnapshotSerializer(createAbsolutePathSerializer()); beforeEach(() => { @@ -118,6 +122,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -145,6 +150,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -172,6 +178,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -201,6 +208,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -227,6 +235,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /x/y/z, @@ -253,6 +262,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -276,6 +286,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -299,6 +310,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -323,6 +335,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -347,6 +360,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -384,18 +398,22 @@ describe('OptimizerConfig::create()', () => { getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); filterById.mockReturnValue(Symbol('filtered bundles')); - jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({ + jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): { + [key in keyof ParsedOptions]: any; + } => ({ cache: Symbol('parsed cache'), dist: Symbol('parsed dist'), maxWorkerCount: Symbol('parsed max worker count'), pluginPaths: Symbol('parsed plugin paths'), pluginScanDirs: Symbol('parsed plugin scan dirs'), repoRoot: Symbol('parsed repo root'), + outputRoot: Symbol('parsed output root'), watch: Symbol('parsed watch'), themeTags: Symbol('theme tags'), inspectWorkers: Symbol('parsed inspect workers'), profileWebpack: Symbol('parsed profile webpack'), filters: [], + includeCoreBundle: false, })); }); @@ -474,6 +492,7 @@ describe('OptimizerConfig::create()', () => { Array [ Symbol(new platform plugins), Symbol(parsed repo root), + Symbol(parsed output root), ], ], "instances": Array [ diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 0e588ab36238..45598ff8831b 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -55,6 +55,13 @@ function omit(obj: T, keys: K[]): Omit { interface Options { /** absolute path to root of the repo/build */ repoRoot: string; + /** + * absolute path to the root directory where output should be written to. This + * defaults to the repoRoot but can be customized to write output somewhere else. + * + * This is how we write output to the build directory in the Kibana build tasks. + */ + outputRoot?: string; /** enable to run the optimizer in watch mode */ watch?: boolean; /** the maximum number of workers that will be created */ @@ -107,8 +114,9 @@ interface Options { themes?: ThemeTag | '*' | ThemeTag[]; } -interface ParsedOptions { +export interface ParsedOptions { repoRoot: string; + outputRoot: string; watch: boolean; maxWorkerCount: number; profileWebpack: boolean; @@ -139,6 +147,11 @@ export class OptimizerConfig { throw new TypeError('repoRoot must be an absolute path'); } + const outputRoot = options.outputRoot ?? repoRoot; + if (!Path.isAbsolute(outputRoot)) { + throw new TypeError('outputRoot must be an absolute path'); + } + /** * BEWARE: this needs to stay roughly synchronized with * `src/core/server/config/env.ts` which determines which paths @@ -182,6 +195,7 @@ export class OptimizerConfig { watch, dist, repoRoot, + outputRoot, maxWorkerCount, profileWebpack, cache, @@ -206,11 +220,11 @@ export class OptimizerConfig { publicDirNames: ['public', 'public/utils'], sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), - outputDir: Path.resolve(options.repoRoot, 'src/core/target/public'), + outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'), }), ] : []), - ...getPluginBundles(plugins, options.repoRoot), + ...getPluginBundles(plugins, options.repoRoot, options.outputRoot), ]; return new OptimizerConfig( diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index ae5d2b5fb329..820b13629697 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -29,7 +29,7 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { Bundle, BundleRefs, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; +import { Bundle, BundleRefs, WorkerConfig, parseDirPath } from '../common'; import { BundleRefsPlugin } from './bundle_refs_plugin'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; @@ -70,7 +70,6 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: plugins: [ new CleanWebpackPlugin(), - new DisallowedSyntaxPlugin(), new BundleRefsPlugin(bundle, bundleRefs), ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 0a9977463aee..7b43d106417b 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -19,6 +19,7 @@ const { parse, resolve } = require('path'); const webpack = require('webpack'); +const webpackMerge = require('webpack-merge'); const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); @@ -26,145 +27,137 @@ const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); const { currentConfig } = require('../../../built_assets/storybook/current.config'); // Extend the Storybook Webpack config with some customizations -module.exports = async ({ config }) => { - // Find and alter the CSS rule to replace the Kibana public path string with a path - // to the route we've added in middleware.js - const cssRule = config.module.rules.find((rule) => rule.test.source.includes('.css$')); - cssRule.use.push({ - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }); - - // Include the React preset from Kibana for Storybook JS files. - config.module.rules.push({ - test: /\.js$/, - exclude: /node_modules/, - loaders: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }); - - config.module.rules.push({ - test: /\.(html|md|txt|tmpl)$/, - use: { - loader: 'raw-loader', - }, - }); - - // Handle Typescript files - config.module.rules.push({ - test: /\.tsx?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], +module.exports = async ({ config: storybookConfig }) => { + let config = { + module: { + rules: [ + // Include the React preset from Kibana for JS(X) and TS(X) + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + loaders: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, }, - }, - ], - }); - - // Parse props data for .tsx files - config.module.rules.push({ - test: /\.tsx$/, - // Exclude example files, as we don't display props info for them - exclude: /\.examples.tsx$/, - use: [ - // Parse TS comments to create Props tables in the UI - require.resolve('react-docgen-typescript-loader'), - ], - }); - - // Enable SASS - config.module.rules.push({ - test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), + { + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', }, }, - }, - { - loader: 'resolve-url-loader', - options: { - // If you don't have arguments (_, __) to the join function, the - // resolve-url-loader fails with a loader misconfiguration error. - // - // eslint-disable-next-line no-unused-vars - join: (_, __) => (uri, base) => { - if (!base || !parse(base).dir.includes('legacy')) { - return null; - } + // Parse props data for .tsx files + // This is notoriously slow, and is making Storybook unusable. Disabling for now. + // See: https://github.com/storybookjs/storybook/issues/7998 + // + // { + // test: /\.tsx$/, + // // Exclude example files, as we don't display props info for them + // exclude: /\.stories.tsx$/, + // use: [ + // // Parse TS comments to create Props tables in the UI + // require.resolve('react-docgen-typescript-loader'), + // ], + // }, + { + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, + }, + }, + { + loader: 'resolve-url-loader', + options: { + // If you don't have arguments (_, __) to the join function, the + // resolve-url-loader fails with a loader misconfiguration error. + // + // eslint-disable-next-line no-unused-vars + join: (_, __) => (uri, base) => { + if (!base || !parse(base).dir.includes('legacy')) { + return null; + } - // URIs on mixins in src/legacy/public/styles need to be resolved. - if (uri.startsWith('ui/assets')) { - return resolve(REPO_ROOT, 'src/core/server/core_app/', uri.replace('ui/', '')); - } + // URIs on mixins in src/legacy/public/styles need to be resolved. + if (uri.startsWith('ui/assets')) { + return resolve(REPO_ROOT, 'src/core/server/core_app/', uri.replace('ui/', '')); + } - return null; - }, + return null; + }, + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') + )};\n`; + }, + sassOptions: { + includePaths: [resolve(REPO_ROOT, 'node_modules')], + }, + }, + }, + ], }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') - )};\n`; + ], + }, + plugins: [ + // Reference the built DLL file of static(ish) dependencies, which are removed + // during kbn:bootstrap and rebuilt if missing. + new webpack.DllReferencePlugin({ + manifest: resolve(DLL_DIST_DIR, 'manifest.json'), + context: REPO_ROOT, + }), + // Copy the DLL files to the Webpack build for use in the Storybook UI + + new CopyWebpackPlugin({ + patterns: [ + { + from: resolve(DLL_DIST_DIR, 'dll.js'), + to: 'dll.js', }, - sassOptions: { - includePaths: [resolve(REPO_ROOT, 'node_modules')], + { + from: resolve(DLL_DIST_DIR, 'dll.css'), + to: 'dll.css', }, - }, - }, + ], + }), ], - }); - - // Reference the built DLL file of static(ish) dependencies, which are removed - // during kbn:bootstrap and rebuilt if missing. - config.plugins.push( - new webpack.DllReferencePlugin({ - manifest: resolve(DLL_DIST_DIR, 'manifest.json'), - context: REPO_ROOT, - }) - ); + resolve: { + // Tell Webpack about the ts/x extensions + extensions: ['.ts', '.tsx', '.scss'], + }, + }; - // Copy the DLL files to the Webpack build for use in the Storybook UI - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ - { - from: resolve(DLL_DIST_DIR, 'dll.js'), - to: 'dll.js', - }, - { - from: resolve(DLL_DIST_DIR, 'dll.css'), - to: 'dll.css', - }, - ], - }) - ); + // Find and alter the CSS rule to replace the Kibana public path string with a path + // to the route we've added in middleware.js + const cssRule = storybookConfig.module.rules.find((rule) => rule.test.source.includes('.css$')); + cssRule.use.push({ + loader: 'string-replace-loader', + options: { + search: '__REPLACE_WITH_PUBLIC_PATH__', + replace: '/', + flags: 'g', + }, + }); - // Tell Webpack about the ts/x extensions - config.resolve.extensions.push('.ts', '.tsx', '.scss'); + config = webpackMerge(storybookConfig, config); // Load custom Webpack config specified by a plugin. if (currentConfig.webpackHook) { // eslint-disable-next-line import/no-dynamic-require - config = await require(currentConfig.webpackHook)({ config }); + return await require(currentConfig.webpackHook)({ config }); } return config; diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index d630fec652a3..3f34742e4486 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -1082,7 +1082,7 @@ const { body } = await client.asInternalUser.get({ id: 'id' }); const { body } = await client.asInternalUser.get({ id: 'id' }); ``` -- the returned error types changed +- the returned error types changed There are no longer specific errors for every HTTP status code (such as `BadRequest` or `NotFound`). A generic `ResponseError` with the specific `statusCode` is thrown instead. @@ -1097,6 +1097,7 @@ try { if(e instanceof errors.NotFound) { // do something } + if(e.status === 401) {} } ``` @@ -1115,6 +1116,7 @@ try { if(e.name === 'ResponseError' && e.statusCode === 404) { // do something } + if(e.statusCode === 401) {...} } ``` @@ -1178,6 +1180,30 @@ const request = client.asCurrentUser.ping({}, { }); ``` +- the new client doesn't provide exhaustive typings for the response object yet. You might have to copy +response type definitions from the Legacy Elasticsearch library until https://github.com/elastic/elasticsearch-js/pull/970 merged. + +```ts +// platform provides a few typings for internal purposes +import { SearchResponse } from 'src/core/server'; +type SearchSource = {...}; +type SearchBody = SearchResponse; +const { body } = await client.search(...); +interface Info {...} +const { body } = await client.info(...); +``` + +- Functional tests are subject to migration to the new client as well. +before: +```ts +const client = getService('legacyEs'); +``` + +after: +```ts +const client = getService('es'); +``` + Please refer to the [Breaking changes list](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/breaking-changes.html) for more information about the changes between the legacy and new client. diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index beb5ad40229d..48625078e9bd 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter, REPO_ROOT } from '@kbn/dev-utils'; import { runOptimizer, OptimizerConfig, @@ -29,9 +29,10 @@ import { Task } from '../lib'; export const BuildKibanaPlatformPlugins: Task = { description: 'Building distributable versions of Kibana platform plugins', - async run(config, log, build) { - const optimizerConfig = OptimizerConfig.create({ - repoRoot: build.resolvePath(), + async run(_, log, build) { + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + outputRoot: build.resolvePath(), cache: false, oss: build.isOss(), examples: false, @@ -42,11 +43,10 @@ export const BuildKibanaPlatformPlugins: Task = { const reporter = CiStatsReporter.fromEnv(log); - await runOptimizer(optimizerConfig) - .pipe( - reportOptimizerStats(reporter, optimizerConfig, log), - logOptimizerState(log, optimizerConfig) - ) + await runOptimizer(config) + .pipe(reportOptimizerStats(reporter, config, log), logOptimizerState(log, config)) .toPromise(); + + await Promise.all(config.bundles.map((b) => b.cache.clear())); }, }; diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index c8489673b83a..79279997671e 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -30,7 +30,7 @@ export const CopySource: Task = { 'src/**', '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files - '!src/**/{__tests__,__snapshots__,__mocks__}/**', + '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', '!src/test_utils/**', '!src/fixtures/**', '!src/legacy/core_plugins/console/public/tests/**', diff --git a/src/dev/ci_setup/checkout_sibling_es.sh b/src/dev/ci_setup/checkout_sibling_es.sh index 915759d4214f..3832ec9b4076 100755 --- a/src/dev/ci_setup/checkout_sibling_es.sh +++ b/src/dev/ci_setup/checkout_sibling_es.sh @@ -7,10 +7,11 @@ function checkout_sibling { targetDir=$2 useExistingParamName=$3 useExisting="$(eval "echo "\$$useExistingParamName"")" + repoAddress="https://github.com/" if [ -z ${useExisting:+x} ]; then if [ -d "$targetDir" ]; then - echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$PARENT_DIR]!" + echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$WORKSPACE]!" echo echo "Either define '${useExistingParamName}' or remove the existing '${project}' sibling." exit 1 @@ -21,8 +22,9 @@ function checkout_sibling { cloneBranch="" function clone_target_is_valid { + echo " -> checking for '${cloneBranch}' branch at ${cloneAuthor}/${project}" - if [[ -n "$(git ls-remote --heads "git@github.com:${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then + if [[ -n "$(git ls-remote --heads "${repoAddress}${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then return 0 else return 1 @@ -71,7 +73,7 @@ function checkout_sibling { fi echo " -> checking out '${cloneBranch}' branch from ${cloneAuthor}/${project}..." - git clone -b "$cloneBranch" "git@github.com:${cloneAuthor}/${project}.git" "$targetDir" --depth=1 + git clone -b "$cloneBranch" "${repoAddress}${cloneAuthor}/${project}.git" "$targetDir" --depth=1 echo " -> checked out ${project} revision: $(git -C "${targetDir}" rev-parse HEAD)" echo } @@ -87,12 +89,12 @@ function checkout_sibling { fi } -checkout_sibling "elasticsearch" "${PARENT_DIR}/elasticsearch" "USE_EXISTING_ES" +checkout_sibling "elasticsearch" "${WORKSPACE}/elasticsearch" "USE_EXISTING_ES" export TEST_ES_FROM=${TEST_ES_FROM:-snapshot} # Set the JAVA_HOME based on the Java property file in the ES repo # This assumes the naming convention used on CI (ex: ~/.java/java10) -ES_DIR="$PARENT_DIR/elasticsearch" +ES_DIR="$WORKSPACE/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 86927b694679..72ec73ad810e 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -53,6 +53,8 @@ export PARENT_DIR="$parentDir" kbnBranch="$(jq -r .branch "$KIBANA_DIR/package.json")" export KIBANA_PKG_BRANCH="$kbnBranch" +export WORKSPACE="${WORKSPACE:-$PARENT_DIR}" + ### ### download node ### @@ -162,7 +164,7 @@ export -f checks-reporter-with-killswitch source "$KIBANA_DIR/src/dev/ci_setup/load_env_keys.sh" -ES_DIR="$PARENT_DIR/elasticsearch" +ES_DIR="$WORKSPACE/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 37bbcce72e49..0bef5bc5f32d 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -49,8 +49,10 @@ export async function generateNoticeFromSource({ productName, directory, log }: ignore: [ '{node_modules,build,dist,data,built_assets}/**', 'packages/*/{node_modules,build,dist}/**', + 'src/plugins/*/{node_modules,build,dist}/**', 'x-pack/{node_modules,build,dist,data}/**', 'x-pack/packages/*/{node_modules,build,dist}/**', + 'x-pack/plugins/*/{node_modules,build,dist}/**', '**/target/**', ], }; diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 404ad6717468..36d0ff8f51d8 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -177,12 +177,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/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/common/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/legacy/core_plugins/kibana/common/utils/no_white_space.js b/src/legacy/core_plugins/kibana/common/utils/no_white_space.js deleted file mode 100644 index 580418eb3423..000000000000 --- a/src/legacy/core_plugins/kibana/common/utils/no_white_space.js +++ /dev/null @@ -1,37 +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 TAGS_WITH_WS = />\s+<'); -} 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 e1dadb0a24de..625c2c02510d 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -258,20 +258,6 @@ export function getUiSettingDefaults() { 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', }), }, - 'timepicker:timeDefaults': { - name: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsTitle', { - defaultMessage: 'Time filter defaults', - }), - value: `{ - "from": "now-15m", - "to": "now" -}`, - type: 'json', - description: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsText', { - defaultMessage: 'The timefilter selection to use when Kibana is started without one', - }), - requiresPageReload: true, - }, 'theme:darkMode': { name: i18n.translate('kbn.advancedSettings.darkModeTitle', { defaultMessage: 'Dark mode', diff --git a/src/plugins/dashboard/README.md b/src/plugins/dashboard/README.md new file mode 100644 index 000000000000..f44bd943eaca --- /dev/null +++ b/src/plugins/dashboard/README.md @@ -0,0 +1 @@ +Contains the dashboard application. \ No newline at end of file diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx index e4a98ffac7a5..0000f63c48c2 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx @@ -69,31 +69,33 @@ export class ReplacePanelFlyout extends React.Component { }; public onReplacePanel = async (savedObjectId: string, type: string, name: string) => { - const originalPanels = this.props.container.getInput().panels; - const filteredPanels = { ...originalPanels }; + const { panelToRemove, container } = this.props; + const { w, h, x, y } = (container.getInput().panels[ + panelToRemove.id + ] as DashboardPanelState).gridData; - const nnw = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.w; - const nnh = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.h; - const nnx = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.x; - const nny = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.y; - - // add the new view - const newObj = await this.props.container.addNewEmbeddable(type, { + const { id } = await container.addNewEmbeddable(type, { savedObjectId, }); - const finalPanels = _.cloneDeep(this.props.container.getInput().panels); - (finalPanels[newObj.id] as DashboardPanelState).gridData.w = nnw; - (finalPanels[newObj.id] as DashboardPanelState).gridData.h = nnh; - (finalPanels[newObj.id] as DashboardPanelState).gridData.x = nnx; - (finalPanels[newObj.id] as DashboardPanelState).gridData.y = nny; - - // delete the old view - delete finalPanels[this.props.panelToRemove.id]; - - // apply changes - this.props.container.updateInput({ panels: finalPanels }); - this.props.container.reload(); + const { [panelToRemove.id]: omit, ...panels } = container.getInput().panels; + + container.updateInput({ + panels: { + ...panels, + [id]: { + ...panels[id], + gridData: { + ...(panels[id] as DashboardPanelState).gridData, + w, + h, + x, + y, + }, + } as DashboardPanelState, + }, + }); + container.reload(); this.showToast(name); this.props.onClose(); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d6812a4aa452..2cfdab80123e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1661,7 +1661,7 @@ export class SearchInterceptor { // (undocumented) protected readonly requestTimeout?: number | undefined; // (undocumented) - protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable; + protected runSearch(request: IEsSearchRequest, signal: AbortSignal): Observable; search(request: IEsSearchRequest, options?: ISearchOptions): Observable; // (undocumented) protected setupTimers(options?: ISearchOptions): { 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 307d1fe1b2b0..2053e0b94b21 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 @@ -50,7 +50,7 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { return true; case UI_SETTINGS.SEARCH_QUERY_LANGUAGE: return 'kuery'; - case 'timepicker:timeDefaults': + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: return { from: 'now-15m', to: 'now' }; case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index df2fbc8e5a8f..35b46de5f21b 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -35,7 +35,7 @@ export interface TimeFilterServiceDependencies { export class TimefilterService { public setup({ uiSettings, storage }: TimeFilterServiceDependencies): TimefilterSetup { const timefilterConfig = { - timeDefaults: uiSettings.get('timepicker:timeDefaults'), + timeDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS), refreshIntervalDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; const history = new TimeHistory(storage); diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts index a9ca9efb8b7e..aaaac5ae6ff7 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts @@ -90,18 +90,4 @@ describe('Search Usage Collector', () => { SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT ); }); - - test('tracks response errors', async () => { - const duration = 10; - await usageCollector.trackError(duration); - expect(mockCoreSetup.http.post).toBeCalled(); - expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); - }); - - test('tracks response duration', async () => { - const duration = 5; - await usageCollector.trackSuccess(duration); - expect(mockCoreSetup.http.post).toBeCalled(); - expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); - }); }); diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts index cb1b2b65c17c..7adb0c3caa67 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts @@ -72,21 +72,5 @@ export const createUsageCollector = ( SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT ); }, - trackError: async (duration: number) => { - return core.http.post('/api/search/usage', { - body: JSON.stringify({ - eventType: 'error', - duration, - }), - }); - }, - trackSuccess: async (duration: number) => { - return core.http.post('/api/search/usage', { - body: JSON.stringify({ - eventType: 'success', - duration, - }), - }); - }, }; }; diff --git a/src/plugins/data/public/search/collectors/types.ts b/src/plugins/data/public/search/collectors/types.ts index bb85532fd3ab..3e98f901eb0c 100644 --- a/src/plugins/data/public/search/collectors/types.ts +++ b/src/plugins/data/public/search/collectors/types.ts @@ -31,6 +31,4 @@ export interface SearchUsageCollector { trackLongQueryPopupShown: () => Promise; trackLongQueryDialogDismissed: () => Promise; trackLongQueryRunBeyondTimeout: () => Promise; - trackError: (duration: number) => Promise; - trackSuccess: (duration: number) => Promise; } diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 84e24114a9e6..e6eca16c5ca4 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -18,7 +18,7 @@ */ import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs'; -import { finalize, filter, tap } from 'rxjs/operators'; +import { finalize, filter } from 'rxjs/operators'; import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public'; import { getCombinedSignal, AbortError } from '../../common/utils'; import { IEsSearchRequest, IEsSearchResponse } from '../../common/search'; @@ -92,16 +92,14 @@ export class SearchInterceptor { protected runSearch( request: IEsSearchRequest, - combinedSignal: AbortSignal + signal: AbortSignal ): Observable { - return from( - this.deps.http.fetch({ - path: `/internal/search/es`, - method: 'POST', - body: JSON.stringify(request), - signal: combinedSignal, - }) - ); + const { id, ...searchRequest } = request; + const path = id != null ? `/internal/search/es/${id}` : '/internal/search/es'; + const method = 'POST'; + const body = JSON.stringify(id != null ? {} : searchRequest); + const response = this.deps.http.fetch({ path, method, body, signal }); + return from(response); } /** @@ -123,13 +121,6 @@ export class SearchInterceptor { this.pendingCount$.next(++this.pendingCount); return this.runSearch(request, combinedSignal).pipe( - tap({ - next: (e) => { - if (this.deps.usageCollector) { - this.deps.usageCollector.trackSuccess(e.rawResponse.took); - } - }, - }), finalize(() => { this.pendingCount$.next(--this.pendingCount); cleanup(); 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 5f2d4c00cd6b..879ff6708068 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 @@ -51,7 +51,7 @@ startMock.uiSettings.get.mockImplementation((key: string) => { return 'MMM D, YYYY @ HH:mm:ss.SSS'; case UI_SETTINGS.HISTORY_LIMIT: return 10; - case 'timepicker:timeDefaults': + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: return { from: 'now-15m', to: 'now', 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 86bf30ba0e37..05249d46a1c5 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 @@ -94,7 +94,7 @@ export function QueryBarTopRow(props: Props) { } function getDateRange() { - const defaultTimeSetting = uiSettings!.get('timepicker:timeDefaults'); + const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); return { from: props.dateRangeFrom || defaultTimeSetting.from, to: props.dateRangeTo || defaultTimeSetting.to, diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 461b21e1cc98..1f3d7fbcb9f0 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -170,6 +170,8 @@ export { ISearchStart, getDefaultSearchParams, getTotalLoaded, + usageProvider, + SearchUsage, } from './search'; // Search namespace diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts b/src/plugins/data/server/search/collectors/index.ts similarity index 93% rename from packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts rename to src/plugins/data/server/search/collectors/index.ts index ca5ba1b90fe9..417dc1c2012d 100644 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts +++ b/src/plugins/data/server/search/collectors/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './disallowed_syntax_plugin'; +export { usageProvider, SearchUsage } from './usage'; diff --git a/src/plugins/data/server/search/collectors/routes.ts b/src/plugins/data/server/search/collectors/routes.ts deleted file mode 100644 index 38fb517e3c3f..000000000000 --- a/src/plugins/data/server/search/collectors/routes.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { schema } from '@kbn/config-schema'; -import { CoreSetup } from '../../../../../core/server'; -import { DataPluginStart } from '../../plugin'; -import { SearchUsage } from './usage'; - -export function registerSearchUsageRoute( - core: CoreSetup, - usage: SearchUsage -): void { - const router = core.http.createRouter(); - - router.post( - { - path: '/api/search/usage', - validate: { - body: schema.object({ - eventType: schema.string(), - duration: schema.number(), - }), - }, - }, - async (context, request, res) => { - const { eventType, duration } = request.body; - - if (eventType === 'success') usage.trackSuccess(duration); - if (eventType === 'error') usage.trackError(duration); - - return res.ok(); - } - ); -} diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts index c43c572c2edb..e1be92aa13c3 100644 --- a/src/plugins/data/server/search/collectors/usage.ts +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -18,19 +18,18 @@ */ import { CoreSetup } from 'kibana/server'; -import { DataPluginStart } from '../../plugin'; import { Usage } from './register'; const SAVED_OBJECT_ID = 'search-telemetry'; export interface SearchUsage { - trackError(duration: number): Promise; + trackError(): Promise; trackSuccess(duration: number): Promise; } -export function usageProvider(core: CoreSetup): SearchUsage { +export function usageProvider(core: CoreSetup): SearchUsage { const getTracker = (eventType: keyof Usage) => { - return async (duration: number) => { + return async (duration?: number) => { const repository = await core .getStartServices() .then(([coreStart]) => coreStart.savedObjects.createInternalRepository()); @@ -52,17 +51,17 @@ export function usageProvider(core: CoreSetup): SearchU attributes[eventType]++; - const averageDuration = - (duration + (attributes.averageDuration ?? 0)) / - ((attributes.errorCount ?? 0) + (attributes.successCount ?? 0)); - - const newAttributes = { ...attributes, averageDuration }; + // Only track the average duration for successful requests + if (eventType === 'successCount') { + attributes.averageDuration = + ((duration ?? 0) + (attributes.averageDuration ?? 0)) / (attributes.successCount ?? 1); + } try { if (doesSavedObjectExist) { - await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, newAttributes); + await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, attributes); } else { - await repository.create(SAVED_OBJECT_ID, newAttributes, { id: SAVED_OBJECT_ID }); + await repository.create(SAVED_OBJECT_ID, attributes, { id: SAVED_OBJECT_ID }); } } catch (e) { // Version conflict error, swallow @@ -71,7 +70,7 @@ export function usageProvider(core: CoreSetup): SearchU }; return { - trackError: getTracker('errorCount'), + trackError: () => getTracker('errorCount')(), trackSuccess: getTracker('successCount'), }; } diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index b8010f735c32..78ead6df1a44 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -20,11 +20,13 @@ import { first } from 'rxjs/operators'; import { SharedGlobalConfig, Logger } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Observable } from 'rxjs'; +import { SearchUsage } from '../collectors/usage'; import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded } from '..'; export const esSearchStrategyProvider = ( config$: Observable, - logger: Logger + logger: Logger, + usage?: SearchUsage ): ISearchStrategy => { return { search: async (context, request, options) => { @@ -43,15 +45,22 @@ export const esSearchStrategyProvider = ( ...request.params, }; - const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'search', - params, - options - )) as SearchResponse; + try { + const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'search', + params, + options + )) as SearchResponse; - // The above query will either complete or timeout and throw an error. - // There is no progress indication on this api. - return { rawResponse, ...getTotalLoaded(rawResponse._shards) }; + if (usage) usage.trackSuccess(rawResponse.took); + + // The above query will either complete or timeout and throw an error. + // There is no progress indication on this api. + return { rawResponse, ...getTotalLoaded(rawResponse._shards) }; + } catch (e) { + if (usage) usage.trackError(); + throw e; + } }, }; }; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 67789fcbf56b..cea2714671f0 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -20,3 +20,5 @@ export { ISearchStrategy, ISearchOptions, ISearchSetup, ISearchStart } from './types'; export { getDefaultSearchParams, getTotalLoaded } from './es_search'; + +export { usageProvider, SearchUsage } from './collectors'; diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index bf1982a1f7fb..32d8f8c1b09e 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -27,9 +27,12 @@ export function registerSearchRoute(core: CoreSetup): v router.post( { - path: '/internal/search/{strategy}', + path: '/internal/search/{strategy}/{id?}', validate: { - params: schema.object({ strategy: schema.string() }), + params: schema.object({ + strategy: schema.string(), + id: schema.maybe(schema.string()), + }), query: schema.object({}, { unknowns: 'allow' }), @@ -38,13 +41,13 @@ export function registerSearchRoute(core: CoreSetup): v }, async (context, request, res) => { const searchRequest = request.body; - const { strategy } = request.params; + const { strategy, id } = request.params; const signal = getRequestAbortedSignal(request.events.aborted$); const [, , selfStart] = await core.getStartServices(); try { - const response = await selfStart.search.search(context, searchRequest, { + const response = await selfStart.search.search(context, id ? { id } : searchRequest, { signal, strategy, }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index bbd067175474..9dc47369567a 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -32,7 +32,6 @@ import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; -import { registerSearchUsageRoute } from './collectors/routes'; import { IEsSearchRequest } from '../../common'; interface StrategyMap { @@ -51,9 +50,15 @@ export class SearchService implements Plugin { core: CoreSetup, { usageCollection }: { usageCollection?: UsageCollectionSetup } ): ISearchSetup { + const usage = usageCollection ? usageProvider(core) : undefined; + this.registerSearchStrategy( ES_SEARCH_STRATEGY, - esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$, this.logger) + esSearchStrategyProvider( + this.initializerContext.config.legacy.globalConfig$, + this.logger, + usage + ) ); core.savedObjects.registerType(searchTelemetry); @@ -61,10 +66,7 @@ export class SearchService implements Plugin { registerUsageCollector(usageCollection, this.initializerContext); } - const usage = usageProvider(core); - registerSearchRoute(core); - registerSearchUsageRoute(core, usage); return { registerSearchStrategy: this.registerSearchStrategy, usage }; } diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 25dc890e0257..76afd7e8c951 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -40,7 +40,7 @@ export interface ISearchSetup { /** * Used internally for telemetry */ - usage: SearchUsage; + usage?: SearchUsage; } export interface ISearchStart { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index d35a6a5bbb9a..013034c79d3f 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -32,6 +32,7 @@ import { ClusterRerouteParams } from 'elasticsearch'; import { ClusterStateParams } from 'elasticsearch'; import { ClusterStatsParams } from 'elasticsearch'; import { ConfigOptions } from 'elasticsearch'; +import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CountParams } from 'elasticsearch'; import { CreateDocumentParams } from 'elasticsearch'; import { DeleteDocumentByQueryParams } from 'elasticsearch'; @@ -537,8 +538,7 @@ export interface ISearchOptions { // @public (undocumented) export interface ISearchSetup { registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; - // Warning: (ae-forgotten-export) The symbol "SearchUsage" needs to be exported by the entry point index.d.ts - usage: SearchUsage; + usage?: SearchUsage; } // Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -727,6 +727,16 @@ export const search: { }; }; +// Warning: (ae-missing-release-tag) "SearchUsage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SearchUsage { + // (undocumented) + trackError(): Promise; + // (undocumented) + trackSuccess(duration: number): Promise; +} + // Warning: (ae-missing-release-tag) "shouldReadFieldFromDocValues" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -778,6 +788,11 @@ export const UI_SETTINGS: { readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; }; +// Warning: (ae-missing-release-tag) "usageProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function usageProvider(core: CoreSetup_2): SearchUsage; + // Warnings were encountered during analysis: // @@ -802,13 +817,13 @@ export const UI_SETTINGS: { // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:178:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:179:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:180:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:181:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:182:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:180:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:181:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:182:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" 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 e825ef7f6c94..763a086d7688 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -526,6 +526,24 @@ export function getUiSettings(): Record> { value: schema.number(), }), }, + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + name: i18n.translate('data.advancedSettings.timepicker.timeDefaultsTitle', { + defaultMessage: 'Time filter defaults', + }), + value: `{ + "from": "now-15m", + "to": "now" +}`, + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.timeDefaultsText', { + defaultMessage: 'The timefilter selection to use when Kibana is started without one', + }), + requiresPageReload: true, + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + }, [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: { name: i18n.translate('data.advancedSettings.timepicker.quickRangesTitle', { defaultMessage: 'Time filter quick ranges', diff --git a/src/plugins/discover/README.md b/src/plugins/discover/README.md new file mode 100644 index 000000000000..a914d651eef3 --- /dev/null +++ b/src/plugins/discover/README.md @@ -0,0 +1 @@ +Contains the Discover application and the saved search embeddable. \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 7b862ec518a0..e7fafde2e68d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -17,13 +17,10 @@ * under the License. */ -import _ from 'lodash'; +import { find, template } from 'lodash'; import $ from 'jquery'; -// @ts-ignore import rison from 'rison-node'; import '../../doc_viewer'; -// @ts-ignore -import { noWhiteSpace } from '../../../../../../../legacy/core_plugins/kibana/common/utils/no_white_space'; import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; @@ -35,6 +32,16 @@ import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_he import { esFilters } from '../../../../../../data/public'; import { getServices } from '../../../../kibana_services'; +const TAGS_WITH_WS = />\s+<'); +} + // guesstimate at the minimum number of chars wide cells in the table should be const MIN_LINE_LENGTH = 20; @@ -43,8 +50,8 @@ interface LazyScope extends ng.IScope { } export function createTableRowDirective($compile: ng.ICompileService, $httpParamSerializer: any) { - const cellTemplate = _.template(noWhiteSpace(cellTemplateHtml)); - const truncateByHeightTemplate = _.template(noWhiteSpace(truncateByHeightTemplateHtml)); + const cellTemplate = template(noWhiteSpace(cellTemplateHtml)); + const truncateByHeightTemplate = template(noWhiteSpace(truncateByHeightTemplateHtml)); return { restrict: 'A', @@ -169,7 +176,7 @@ export function createTableRowDirective($compile: ng.ICompileService, $httpParam const $cell = $cells.eq(i); if ($cell.data('discover:html') === html) return; - const reuse = _.find($cells.slice(i + 1), function (cell: any) { + const reuse = find($cells.slice(i + 1), function (cell: any) { return $.data(cell, 'discover:html') === html; }); diff --git a/src/plugins/input_control_vis/README.md b/src/plugins/input_control_vis/README.md new file mode 100644 index 000000000000..67266079dede --- /dev/null +++ b/src/plugins/input_control_vis/README.md @@ -0,0 +1 @@ +Contains the input control visualization allowing to place custom filter controls on a dashboard. \ No newline at end of file diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index ae40b2c92d40..833304378402 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -30,6 +30,7 @@ export class ServiceSettings { constructor(mapConfig, tilemapsConfig) { this._mapConfig = mapConfig; this._tilemapsConfig = tilemapsConfig; + this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; this._showZoomMessage = true; this._emsClient = new EMSClient({ @@ -53,13 +54,10 @@ export class ServiceSettings { linkify: true, }); - // TMS attribution - const attributionFromConfig = _.escape( - markdownIt.render(this._tilemapsConfig.deprecated.config.options.attribution || '') - ); // TMS Options - this.tmsOptionsFromConfig = _.assign({}, this._tilemapsConfig.deprecated.config.options, { - attribution: attributionFromConfig, + this.tmsOptionsFromConfig = _.assign({}, this._tilemapsConfig.options, { + attribution: _.escape(markdownIt.render(this._tilemapsConfig.options.attribution || '')), + url: this._tilemapsConfig.url, }); } @@ -122,7 +120,7 @@ export class ServiceSettings { */ async getTMSServices() { let allServices = []; - if (this._tilemapsConfig.deprecated.isOverridden) { + if (this._hasTmsConfigured) { //use tilemap.* settings from yml const tmsService = _.cloneDeep(this.tmsOptionsFromConfig); tmsService.id = TMS_IN_YML_ID; @@ -210,14 +208,12 @@ export class ServiceSettings { if (tmsServiceConfig.origin === ORIGIN.EMS) { return this._getAttributesForEMSTMSLayer(isDesaturated, isDarkMode); } else if (tmsServiceConfig.origin === ORIGIN.KIBANA_YML) { - const config = this._tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); + const attrs = _.pick(this._tilemapsConfig, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { //this is an older config. need to resolve this dynamically. if (tmsServiceConfig.id === TMS_IN_YML_ID) { - const config = this._tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); + const attrs = _.pick(this._tilemapsConfig, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { //assume ems diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js index 6e416f7fd5c8..b924c3bb5305 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.test.js +++ b/src/plugins/maps_legacy/public/map/service_settings.test.js @@ -49,11 +49,7 @@ describe('service_settings (FKA tile_map test)', function () { }; const defaultTilemapConfig = { - deprecated: { - config: { - options: {}, - }, - }, + options: {}, }; function makeServiceSettings(mapConfigOptions = {}, tilemapOptions = {}) { @@ -160,13 +156,8 @@ describe('service_settings (FKA tile_map test)', function () { serviceSettings = makeServiceSettings( {}, { - deprecated: { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }, + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { minZoom: 0, maxZoom: 20 }, } ); @@ -251,13 +242,8 @@ describe('service_settings (FKA tile_map test)', function () { includeElasticMapsService: false, }, { - deprecated: { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }, + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { minZoom: 0, maxZoom: 20 }, } ); const tilemapServices = await serviceSettings.getTMSServices(); diff --git a/src/plugins/region_map/public/__tests__/region_map_visualization.js b/src/plugins/region_map/public/__tests__/region_map_visualization.js index 0a2a18c7cef4..648193e8e249 100644 --- a/src/plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/plugins/region_map/public/__tests__/region_map_visualization.js @@ -111,12 +111,8 @@ describe('RegionMapsVisualizationTests', function () { emsLandingPageUrl: '', }; const tilemapsConfig = { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, + options: { + attribution: '123', }, }; const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts index 8bffc5d012a7..ad19def16020 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -75,7 +75,7 @@ describe('get_data_telemetry', () => { { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name // New Indexing strategy: everything can be inferred from the constant_keyword values { - name: 'logs-nginx.access-default-000001', + name: '.ds-logs-nginx.access-default-000001', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -84,7 +84,7 @@ describe('get_data_telemetry', () => { sizeInBytes: 1000, }, { - name: 'logs-nginx.access-default-000002', + name: '.ds-logs-nginx.access-default-000002', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -92,6 +92,42 @@ describe('get_data_telemetry', () => { docCount: 1000, sizeInBytes: 60, }, + { + name: '.ds-traces-something-default-000002', + datasetName: 'something', + datasetType: 'traces', + packageName: 'some-package', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: '.ds-metrics-something.else-default-000002', + datasetName: 'something.else', + datasetType: 'metrics', + managedBy: 'ingest-manager', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + // Filter out if it has datasetName and datasetType but none of the shipper, packageName or managedBy === 'ingest-manager' + { + name: 'some-index-that-should-not-show', + datasetName: 'should-not-show', + datasetType: 'logs', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: 'other-index-that-should-not-show', + datasetName: 'should-not-show-either', + datasetType: 'metrics', + managedBy: 'me', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, ]) ).toStrictEqual([ { @@ -138,6 +174,21 @@ describe('get_data_telemetry', () => { doc_count: 2000, size_in_bytes: 1060, }, + { + dataset: { name: 'something', type: 'traces' }, + package: { name: 'some-package' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, + { + dataset: { name: 'something.else', type: 'metrics' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, ]); }); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts index cf906bc5c86c..079f510bb256 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -36,6 +36,9 @@ export interface DataTelemetryDocument extends DataTelemetryBasePayload { name?: string; type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s }; + package?: { + name: string; + }; shipper?: string; pattern_name?: DataPatternName; } @@ -44,6 +47,8 @@ export type DataTelemetryPayload = DataTelemetryDocument[]; export interface DataTelemetryIndex { name: string; + packageName?: string; // Populated by Ingest Manager at `_meta.package.name` + managedBy?: string; // Populated by Ingest Manager at `_meta.managed_by` datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword shipper?: string; // To be obtained from `_meta.beat` if it's set @@ -58,6 +63,7 @@ export interface DataTelemetryIndex { type AtLeastOne }> = Partial & U[keyof U]; type DataDescriptor = AtLeastOne<{ + packageName: string; datasetName: string; datasetType: string; shipper: string; @@ -67,17 +73,28 @@ type DataDescriptor = AtLeastOne<{ function findMatchingDescriptors({ name, shipper, + packageName, + managedBy, datasetName, datasetType, }: DataTelemetryIndex): DataDescriptor[] { // If we already have the data from the indices' mappings... - if ([shipper, datasetName, datasetType].some(Boolean)) { + if ( + [shipper, packageName].some(Boolean) || + (managedBy === 'ingest-manager' && [datasetType, datasetName].some(Boolean)) + ) { return [ { ...(shipper && { shipper }), + ...(packageName && { packageName }), ...(datasetName && { datasetName }), ...(datasetType && { datasetType }), - } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause + } as AtLeastOne<{ + packageName: string; + datasetName: string; + datasetType: string; + shipper: string; + }>, // Using casting here because TS doesn't infer at least one exists from the if clause ]; } @@ -122,6 +139,7 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe ({ name }) => !( name.startsWith('.') && + !name.startsWith('.ds-') && // data_stream-related indices can be included !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern)) ) ); @@ -130,10 +148,17 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe for (const indexCandidate of indexCandidates) { const matchingDescriptors = findMatchingDescriptors(indexCandidate); - for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) { - const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`; + for (const { + datasetName, + datasetType, + packageName, + shipper, + patternName, + } of matchingDescriptors) { + const key = `${datasetName}-${datasetType}-${packageName}-${shipper}-${patternName}`; acc.set(key, { ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...(packageName && { package: { name: packageName } }), ...(shipper && { shipper }), ...(patternName && { pattern_name: patternName }), ...increaseCounters(acc.get(key), indexCandidate), @@ -165,6 +190,12 @@ interface IndexMappings { mappings: { _meta?: { beat?: string; + + // Ingest Manager provided metadata + package?: { + name?: string; + }; + managed_by?: string; // Typically "ingest-manager" }; properties: { dataset?: { @@ -195,7 +226,7 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { try { const index = [ ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), - '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter} + '*-*-*', // Include data-streams aliases `{type}-{dataset}-{namespace}` ]; const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value @@ -204,16 +235,17 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { filterPath: [ // _meta.beat tells the shipper '*.mappings._meta.beat', + // _meta.package.name tells the Ingest Manager's package + '*.mappings._meta.package.name', + // _meta.managed_by is usually populated by Ingest Manager for the UI to identify it + '*.mappings._meta.managed_by', // Does it have `ecs.version` in the mappings? => It follows the ECS conventions '*.mappings.properties.ecs.properties.version.type', - // Disable the fields below because they are still pending to be confirmed: - // https://github.com/elastic/ecs/pull/845 - // TODO: Re-enable when the final fields are confirmed - // // If `dataset.type` is a `constant_keyword`, it can be reported as a type - // '*.mappings.properties.dataset.properties.type.value', - // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset - // '*.mappings.properties.dataset.properties.name.value', + // If `dataset.type` is a `constant_keyword`, it can be reported as a type + '*.mappings.properties.dataset.properties.type.value', + // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset + '*.mappings.properties.dataset.properties.name.value', ], }), // GET /_stats/docs,store?level=indices&filter_path=indices.*.total @@ -227,24 +259,25 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); const indices = indexNames.map((name) => { - const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type; - const shipper = indexMappings[name]?.mappings?._meta?.beat; - const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value; - const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value; + const baseIndexInfo = { + name, + isECS: !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type, + shipper: indexMappings[name]?.mappings?._meta?.beat, + packageName: indexMappings[name]?.mappings?._meta?.package?.name, + managedBy: indexMappings[name]?.mappings?._meta?.managed_by, + datasetName: indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value, + datasetType: indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value, + }; const stats = (indexStats?.indices || {})[name]; if (stats) { return { - name, - datasetName, - datasetType, - shipper, - isECS, + ...baseIndexInfo, docCount: stats.total?.docs?.count, sizeInBytes: stats.total?.store?.size_in_bytes, }; } - return { name, datasetName, datasetType, shipper, isECS }; + return baseIndexInfo; }); return buildDataTelemetryPayload(indices); } catch (e) { diff --git a/src/plugins/tile_map/config.ts b/src/plugins/tile_map/config.ts index 435e52103d15..e754c8429111 100644 --- a/src/plugins/tile_map/config.ts +++ b/src/plugins/tile_map/config.ts @@ -21,15 +21,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ url: schema.maybe(schema.string()), - deprecated: schema.any({ - defaultValue: { - config: { - options: { - attribution: '', - }, - }, - }, - }), options: schema.object({ attribution: schema.string({ defaultValue: '' }), minZoom: schema.number({ defaultValue: 0, min: 0 }), diff --git a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 9ff25ce674d3..f2830e58e0ee 100644 --- a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -98,12 +98,8 @@ describe('CoordinateMapsVisualizationTest', function () { emsLandingPageUrl: '', }; const tilemapsConfig = { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, + options: { + attribution: '123', }, }; diff --git a/src/plugins/tile_map/server/index.ts b/src/plugins/tile_map/server/index.ts index 3381553fe936..4bf8c98c99d2 100644 --- a/src/plugins/tile_map/server/index.ts +++ b/src/plugins/tile_map/server/index.ts @@ -23,7 +23,6 @@ import { configSchema, ConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { exposeToBrowser: { url: true, - deprecated: true, options: true, }, schema: configSchema, diff --git a/src/plugins/timelion/README.md b/src/plugins/timelion/README.md new file mode 100644 index 000000000000..d29a33028e96 --- /dev/null +++ b/src/plugins/timelion/README.md @@ -0,0 +1,2 @@ +Contains the deprecated timelion application. For the timelion visualization, +which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. diff --git a/src/plugins/vis_type_markdown/README.md b/src/plugins/vis_type_markdown/README.md new file mode 100644 index 000000000000..ae79a4822d4a --- /dev/null +++ b/src/plugins/vis_type_markdown/README.md @@ -0,0 +1 @@ +The markdown visualization that can be used to place text panels on dashboards. \ No newline at end of file diff --git a/src/plugins/vis_type_metric/README.md b/src/plugins/vis_type_metric/README.md new file mode 100644 index 000000000000..78df92832bdb --- /dev/null +++ b/src/plugins/vis_type_metric/README.md @@ -0,0 +1 @@ +Contains the metric visualization. \ No newline at end of file diff --git a/src/plugins/vis_type_table/README.md b/src/plugins/vis_type_table/README.md new file mode 100644 index 000000000000..cf37e133ed1c --- /dev/null +++ b/src/plugins/vis_type_table/README.md @@ -0,0 +1 @@ +Contains the data table visualization, that allows presenting data in a simple table format. \ No newline at end of file diff --git a/src/plugins/vis_type_tagcloud/README.md b/src/plugins/vis_type_tagcloud/README.md new file mode 100644 index 000000000000..7e8f2a6e5b72 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/README.md @@ -0,0 +1 @@ +Contains the tagcloud visualization. \ No newline at end of file diff --git a/src/plugins/vis_type_timelion/README.md b/src/plugins/vis_type_timelion/README.md index c306e03abf2c..89d34527c51d 100644 --- a/src/plugins/vis_type_timelion/README.md +++ b/src/plugins/vis_type_timelion/README.md @@ -1,5 +1,7 @@ # Vis type Timelion +Contains the timelion visualization and the timelion backend. + # Generate a parser If your grammar was changed in `public/chain.peg` you need to re-generate the static parser. You could use a grunt task: diff --git a/src/plugins/vis_type_timeseries/README.md b/src/plugins/vis_type_timeseries/README.md new file mode 100644 index 000000000000..4b4184b6eadd --- /dev/null +++ b/src/plugins/vis_type_timeseries/README.md @@ -0,0 +1 @@ +Contains everything around TSVB (the editor, visualizatin implementations and backends). \ No newline at end of file diff --git a/src/plugins/vis_type_vega/README.md b/src/plugins/vis_type_vega/README.md new file mode 100644 index 000000000000..3d9bfd387e2c --- /dev/null +++ b/src/plugins/vis_type_vega/README.md @@ -0,0 +1 @@ +Contains the Vega visualization. \ No newline at end of file diff --git a/src/plugins/vis_type_vislib/README.md b/src/plugins/vis_type_vislib/README.md new file mode 100644 index 000000000000..7641ea2acd1e --- /dev/null +++ b/src/plugins/vis_type_vislib/README.md @@ -0,0 +1,2 @@ +Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and +heatmap charts. diff --git a/src/plugins/vis_type_xy/README.md b/src/plugins/vis_type_xy/README.md new file mode 100644 index 000000000000..70ddb21c1e9d --- /dev/null +++ b/src/plugins/vis_type_xy/README.md @@ -0,0 +1,2 @@ +Contains the new xy-axis chart using the elastic-charts library, which will eventually +replace the vislib xy-axis (bar, area, line) charts. \ No newline at end of file diff --git a/src/plugins/visualizations/README.md b/src/plugins/visualizations/README.md new file mode 100644 index 000000000000..c61beb670a50 --- /dev/null +++ b/src/plugins/visualizations/README.md @@ -0,0 +1,2 @@ +Contains most of the visualization infrastructure, e.g. the visualization type registry or the +visualization embeddable. \ No newline at end of file diff --git a/src/plugins/visualize/README.md b/src/plugins/visualize/README.md new file mode 100644 index 000000000000..be3e555a1407 --- /dev/null +++ b/src/plugins/visualize/README.md @@ -0,0 +1,2 @@ +Contains the visualize application which includes the listing page and the app frame, +which will load the visualization's editor. \ No newline at end of file diff --git a/tasks/test_jest.js b/tasks/test_jest.js index d8f51806e8dd..810ed4232484 100644 --- a/tasks/test_jest.js +++ b/tasks/test_jest.js @@ -22,7 +22,7 @@ const { resolve } = require('path'); module.exports = function (grunt) { grunt.registerTask('test:jest', function () { const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest.js')).then(done, done); + runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done); }); grunt.registerTask('test:jest_integration', function () { @@ -30,10 +30,10 @@ module.exports = function (grunt) { runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done); }); - function runJest(jestScript) { + function runJest(jestScript, args = []) { const serverCmd = { cmd: 'node', - args: [jestScript, '--ci'], + args: [jestScript, '--ci', ...args], opts: { stdio: 'inherit' }, }; diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 7e1f88650cbb..f1082bf618b9 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -130,7 +130,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const fromTime = 'Oct 22, 2018 @ 00:00:00.000'; const toTime = 'Oct 28, 2018 @ 23:59:59.999'; // Sometimes popovers take some time to appear in Firefox (#71979) - await retry.try(async () => { + await retry.tryForTime(20000, async () => { await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.visualBuilder.setIndexPatternValue('kibana_sample_data_flights'); await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp'); diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh new file mode 100755 index 000000000000..503d12b2f6d7 --- /dev/null +++ b/test/scripts/checks/doc_api_changes.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkDocApiChanges diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh new file mode 100755 index 000000000000..513664263791 --- /dev/null +++ b/test/scripts/checks/file_casing.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkFileCasing diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh new file mode 100755 index 000000000000..7a6fd46c46c7 --- /dev/null +++ b/test/scripts/checks/i18n.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:i18nCheck diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh new file mode 100755 index 000000000000..a08d7d07a24a --- /dev/null +++ b/test/scripts/checks/licenses.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:licenses diff --git a/test/scripts/checks/lock_file_symlinks.sh b/test/scripts/checks/lock_file_symlinks.sh new file mode 100755 index 000000000000..1d43d32c9feb --- /dev/null +++ b/test/scripts/checks/lock_file_symlinks.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkLockfileSymlinks diff --git a/test/scripts/checks/telemetry.sh b/test/scripts/checks/telemetry.sh new file mode 100755 index 000000000000..c74ec295b385 --- /dev/null +++ b/test/scripts/checks/telemetry.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:telemetryCheck diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh new file mode 100755 index 000000000000..918475857765 --- /dev/null +++ b/test/scripts/checks/test_hardening.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_hardening diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh new file mode 100755 index 000000000000..5f9aafe80e10 --- /dev/null +++ b/test/scripts/checks/test_projects.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_projects diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh new file mode 100755 index 000000000000..d667c753baec --- /dev/null +++ b/test/scripts/checks/ts_projects.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkTsProjects diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh new file mode 100755 index 000000000000..07c49638134b --- /dev/null +++ b/test/scripts/checks/type_check.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:typeCheck diff --git a/test/scripts/checks/verify_dependency_versions.sh b/test/scripts/checks/verify_dependency_versions.sh new file mode 100755 index 000000000000..b73a71e7ff7f --- /dev/null +++ b/test/scripts/checks/verify_dependency_versions.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:verifyDependencyVersions diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh new file mode 100755 index 000000000000..9f8343e54086 --- /dev/null +++ b/test/scripts/checks/verify_notice.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:verifyNotice diff --git a/test/scripts/jenkins_accessibility.sh b/test/scripts/jenkins_accessibility.sh index c122d71b58ed..fa7cbd41d707 100755 --- a/test/scripts/jenkins_accessibility.sh +++ b/test/scripts/jenkins_accessibility.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_oss.sh checks-reporter-with-killswitch "Kibana accessibility tests" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/accessibility/config.ts; diff --git a/test/scripts/jenkins_build_kbn_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_sample_panel_action.sh old mode 100644 new mode 100755 diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 2310a35f94f3..f449986713f9 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,13 +2,9 @@ source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --oss \ - --filter '!alertingExample' \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ - --verbose; +if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + ./test/scripts/jenkins_build_plugins.sh +fi # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -20,4 +16,7 @@ yarn run grunt functionalTests:ensureAllTestsInCiGroup; if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + + mkdir -p "$WORKSPACE/kibana-build-oss" + cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ fi diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh new file mode 100755 index 000000000000..0c3ee4e3f261 --- /dev/null +++ b/test/scripts/jenkins_build_plugins.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --filter '!alertingExample' \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --workers 6 \ + --verbose diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 60d7f0406f4c..2542d7032e83 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -5,7 +5,7 @@ source test/scripts/jenkins_test_setup_oss.sh if [[ -z "$CODE_COVERAGE" ]]; then checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; - if [ "$CI_GROUP" == "1" ]; then + if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh yarn run grunt run:pluginFunctionalTestsRelease --from=source; yarn run grunt run:exampleFunctionalTestsRelease --from=source; diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh index 2bba6e06d76d..247ab360b791 100755 --- a/test/scripts/jenkins_firefox_smoke.sh +++ b/test/scripts/jenkins_firefox_smoke.sh @@ -5,6 +5,6 @@ source test/scripts/jenkins_test_setup_oss.sh checks-reporter-with-killswitch "Firefox smoke test" \ node scripts/functional_tests \ --bail --debug \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --include-tag "includeFirefox" \ --config test/functional/config.firefox.js; diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh new file mode 100755 index 000000000000..1d691d98982d --- /dev/null +++ b/test/scripts/jenkins_plugin_functional.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +cd test/plugin_functional/plugins/kbn_sample_panel_action; +if [[ ! -d "target" ]]; then + yarn build; +fi +cd -; + +pwd + +yarn run grunt run:pluginFunctionalTestsRelease --from=source; +yarn run grunt run:exampleFunctionalTestsRelease --from=source; +yarn run grunt run:interpreterFunctionalTestsRelease; diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh old mode 100644 new mode 100755 index 204911a3eeda..a5a1a2103801 --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -1,12 +1,6 @@ #!/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" +source test/scripts/jenkins_test_setup_xpack.sh echo " -> Running security solution cypress tests" cd "$XPACK_DIR" diff --git a/test/scripts/jenkins_setup_parallel_workspace.sh b/test/scripts/jenkins_setup_parallel_workspace.sh new file mode 100755 index 000000000000..5274d05572e7 --- /dev/null +++ b/test/scripts/jenkins_setup_parallel_workspace.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e + +CURRENT_DIR=$(pwd) + +# Copy everything except node_modules into the current workspace +rsync -a ${WORKSPACE}/kibana/* . --exclude node_modules +rsync -a ${WORKSPACE}/kibana/.??* . + +# Symlink all non-root, non-fixture node_modules into our new workspace +cd ${WORKSPACE}/kibana +find . -type d -name node_modules -not -path '*__fixtures__*' -not -path './node_modules*' -prune -print0 | xargs -0I % ln -s "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" +find . -type d -wholename '*__fixtures__*node_modules' -not -path './node_modules*' -prune -print0 | xargs -0I % cp -R "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" +cd "${CURRENT_DIR}" + +# Symlink all of the individual root-level node_modules into the node_modules/ directory +mkdir -p node_modules +ln -s ${WORKSPACE}/kibana/node_modules/* node_modules/ +ln -s ${WORKSPACE}/kibana/node_modules/.??* node_modules/ + +# Copy a few node_modules instead of symlinking them. They don't work correctly if symlinked +unlink node_modules/@kbn +unlink node_modules/css-loader +unlink node_modules/style-loader + +# packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts will fail if this is a symlink +unlink node_modules/val-loader + +cp -R ${WORKSPACE}/kibana/node_modules/@kbn node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/css-loader node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/style-loader node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/val-loader node_modules/ diff --git a/test/scripts/jenkins_test_setup.sh b/test/scripts/jenkins_test_setup.sh old mode 100644 new mode 100755 index 49ee8a6b526c..05b88aa2dd0a --- a/test/scripts/jenkins_test_setup.sh +++ b/test/scripts/jenkins_test_setup.sh @@ -14,3 +14,9 @@ trap 'post_work' EXIT export TEST_BROWSER_HEADLESS=1 source src/dev/ci_setup/setup_env.sh + +# For parallel workspaces, we should copy the .es directory from the root, because it should already have downloaded snapshots in it +# This isn't part of jenkins_setup_parallel_workspace.sh just because not all tasks require ES +if [[ ! -d .es && -d "$WORKSPACE/kibana/.es" ]]; then + cp -R $WORKSPACE/kibana/.es ./ +fi diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh old mode 100644 new mode 100755 index 7bbb86752638..b7eac33f3517 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -2,10 +2,17 @@ source test/scripts/jenkins_test_setup.sh -if [[ -z "$CODE_COVERAGE" ]] ; then - installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" - destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER} - cp -R "$installDir" "$destDir" +if [[ -z "$CODE_COVERAGE" ]]; then + + destDir="build/kibana-build-oss" + if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" + fi + + if [[ ! -d $destDir ]]; then + mkdir -p $destDir + cp -pR "$WORKSPACE/kibana-build-oss/." $destDir/ + fi export KIBANA_INSTALL_DIR="$destDir" fi diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh old mode 100644 new mode 100755 index a72e9749ebbd..74a3de77e3a7 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,11 +3,18 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - installDir="$PARENT_DIR/install/kibana" - destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}" - cp -R "$installDir" "$destDir" - export KIBANA_INSTALL_DIR="$destDir" + destDir="build/kibana-build-xpack" + if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" + fi + + if [[ ! -d $destDir ]]; then + mkdir -p $destDir + cp -pR "$WORKSPACE/kibana-build-xpack/." $destDir/ + fi + + export KIBANA_INSTALL_DIR="$(realpath $destDir)" cd "$XPACK_DIR" fi diff --git a/test/scripts/jenkins_xpack_accessibility.sh b/test/scripts/jenkins_xpack_accessibility.sh index a3c03dd78088..3afd4bfb7639 100755 --- a/test/scripts/jenkins_xpack_accessibility.sh +++ b/test/scripts/jenkins_xpack_accessibility.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_xpack.sh checks-reporter-with-killswitch "X-Pack accessibility tests" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/accessibility/config.ts; diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index c962b962b1e5..2452e2f5b8c5 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,15 +3,9 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ - --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ - --verbose; +if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + ./test/scripts/jenkins_xpack_build_plugins.sh +fi # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -36,7 +30,10 @@ if [[ -z "$CODE_COVERAGE" ]] ; then cd "$KIBANA_DIR" node scripts/build --debug --no-oss linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" - installDir="$PARENT_DIR/install/kibana" + installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + + mkdir -p "$WORKSPACE/kibana-build-xpack" + cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ fi diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh new file mode 100755 index 000000000000..3fd3d02de130 --- /dev/null +++ b/test/scripts/jenkins_xpack_build_plugins.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --workers 12 \ + --verbose diff --git a/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh b/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh index d3ca8839a7da..e3b0fe778bdf 100755 --- a/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh +++ b/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_xpack.sh checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/saved_objects_field_count/config.ts; diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index 7fb7d7b71b2e..55d4a524820c 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -7,19 +7,19 @@ echo " -> building and extracting default Kibana distributable" cd "$KIBANA_DIR" node scripts/build --debug --no-oss linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" -installDir="$PARENT_DIR/install/kibana" +installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 +mkdir -p "$WORKSPACE/kibana-build-xpack" +cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ + # cd "$KIBANA_DIR" # source "test/scripts/jenkins_xpack_page_load_metrics.sh" cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" -cd "$KIBANA_DIR" -source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" - echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" yarn percy exec -t 10000 -- -- \ diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh new file mode 100755 index 000000000000..c3211300b96c --- /dev/null +++ b/test/scripts/lint/eslint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:eslint diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh new file mode 100755 index 000000000000..b9c683bcb049 --- /dev/null +++ b/test/scripts/lint/sasslint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:sasslint diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh new file mode 100755 index 000000000000..152c97a3ca7d --- /dev/null +++ b/test/scripts/test/api_integration.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:apiIntegrationTests diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh new file mode 100755 index 000000000000..73dbbddfb38f --- /dev/null +++ b/test/scripts/test/jest_integration.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_jest_integration diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh new file mode 100755 index 000000000000..e25452698ceb --- /dev/null +++ b/test/scripts/test/jest_unit.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_jest diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh new file mode 100755 index 000000000000..e9985300ba19 --- /dev/null +++ b/test/scripts/test/karma_ci.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_karma_ci diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh new file mode 100755 index 000000000000..43c00f0a09dc --- /dev/null +++ b/test/scripts/test/mocha.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:mocha diff --git a/test/scripts/test/safer_lodash_set.sh b/test/scripts/test/safer_lodash_set.sh new file mode 100755 index 000000000000..4d7f9c28210d --- /dev/null +++ b/test/scripts/test/safer_lodash_set.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_package_safer_lodash_set diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh new file mode 100755 index 000000000000..93d70ec35539 --- /dev/null +++ b/test/scripts/test/xpack_jest_unit.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=10 diff --git a/test/scripts/test/xpack_karma.sh b/test/scripts/test/xpack_karma.sh new file mode 100755 index 000000000000..9078f01f1b87 --- /dev/null +++ b/test/scripts/test/xpack_karma.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma diff --git a/test/scripts/test/xpack_list_cyclic_dependency.sh b/test/scripts/test/xpack_list_cyclic_dependency.sh new file mode 100755 index 000000000000..493fe9f58d32 --- /dev/null +++ b/test/scripts/test/xpack_list_cyclic_dependency.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps diff --git a/test/scripts/test/xpack_siem_cyclic_dependency.sh b/test/scripts/test/xpack_siem_cyclic_dependency.sh new file mode 100755 index 000000000000..b21301f25ad0 --- /dev/null +++ b/test/scripts/test/xpack_siem_cyclic_dependency.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy index 460a90b8ec0c..2a1b55d83260 100644 --- a/vars/catchErrors.groovy +++ b/vars/catchErrors.groovy @@ -1,8 +1,15 @@ // Basically, this is a shortcut for catchError(catchInterruptions: false) {} // By default, catchError will swallow aborts/timeouts, which we almost never want +// Also, by wrapping it in an additional try/catch, we cut down on spam in Pipeline Steps def call(Map params = [:], Closure closure) { - params.catchInterruptions = false - return catchError(params, closure) + try { + closure() + } catch (ex) { + params.catchInterruptions = false + catchError(params) { + throw ex + } + } } return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 94bfe983b465..173c5b7e1176 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -2,20 +2,63 @@ def withPostBuildReporting(Closure closure) { try { closure() } finally { - catchErrors { - runErrorReporter() + def parallelWorkspaces = [] + try { + parallelWorkspaces = getParallelWorkspaces() + } catch(ex) { + print ex } catchErrors { - runbld.junit() + runErrorReporter([pwd()] + parallelWorkspaces) } catchErrors { publishJunit() } + + catchErrors { + def parallelWorkspace = "${env.WORKSPACE}/parallel" + if (fileExists(parallelWorkspace)) { + dir(parallelWorkspace) { + def workspaceTasks = [:] + + parallelWorkspaces.each { workspaceDir -> + workspaceTasks[workspaceDir] = { + dir(workspaceDir) { + catchErrors { + runbld.junit() + } + } + } + } + + if (workspaceTasks) { + parallel(workspaceTasks) + } + } + } + } } } +def getParallelWorkspaces() { + def workspaces = [] + def parallelWorkspace = "${env.WORKSPACE}/parallel" + if (fileExists(parallelWorkspace)) { + dir(parallelWorkspace) { + // findFiles only returns files if you use glob, so look for a file that should be in every valid workspace + workspaces = findFiles(glob: '*/kibana/package.json') + .collect { + // get the paths to the kibana directories for the parallel workspaces + return parallelWorkspace + '/' + it.path.tokenize('/').dropRight(1).join('/') + } + } + } + + return workspaces +} + def notifyOnError(Closure closure) { try { closure() @@ -35,36 +78,43 @@ def notifyOnError(Closure closure) { } } -def functionalTestProcess(String name, Closure closure) { - return { processNumber -> - def kibanaPort = "61${processNumber}1" - def esPort = "61${processNumber}2" - def esTransportPort = "61${processNumber}3" - def ingestManagementPackageRegistryPort = "61${processNumber}4" +def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { + // This can go away once everything that uses the deprecated workers.parallelProcesses() is moved to task queue + def parallelId = env.TASK_QUEUE_PROCESS_ID ?: env.CI_PARALLEL_PROCESS_NUMBER - withEnv([ - "CI_PARALLEL_PROCESS_NUMBER=${processNumber}", - "TEST_KIBANA_HOST=localhost", - "TEST_KIBANA_PORT=${kibanaPort}", - "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", - "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", - "TEST_ES_TRANSPORT_PORT=${esTransportPort}", - "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", - "IS_PIPELINE_JOB=1", - "JOB=${name}", - "KBN_NP_PLUGINS_BUILT=true", - ]) { - notifyOnError { - closure() - } - } + def kibanaPort = "61${parallelId}1" + def esPort = "61${parallelId}2" + def esTransportPort = "61${parallelId}3" + def ingestManagementPackageRegistryPort = "61${parallelId}4" + + withEnv([ + "CI_GROUP=${parallelId}", + "REMOVE_KIBANA_INSTALL_DIR=1", + "CI_PARALLEL_PROCESS_NUMBER=${parallelId}", + "TEST_KIBANA_HOST=localhost", + "TEST_KIBANA_PORT=${kibanaPort}", + "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", + "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", + "TEST_ES_TRANSPORT_PORT=${esTransportPort}", + "KBN_NP_PLUGINS_BUILT=true", + "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", + ] + additionalEnvs) { + closure() + } +} + +def functionalTestProcess(String name, Closure closure) { + return { + withFunctionalTestEnv(["JOB=${name}"], closure) } } def functionalTestProcess(String name, String script) { return functionalTestProcess(name) { - retryable(name) { - runbld(script, "Execute ${name}") + notifyOnError { + retryable(name) { + runbld(script, "Execute ${name}") + } } } } @@ -120,12 +170,19 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ + '**/target/public/.kbn-optimizer-cache', 'target/kibana-*', + 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', 'target/junit/**/*', - 'test/**/screenshots/**/*.png', + 'target/test-suites-ci-plan.json', + 'test/**/screenshots/session/*.png', + 'test/**/screenshots/failure/*.png', + 'test/**/screenshots/diff/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/**/*.png', + 'x-pack/test/**/screenshots/session/*.png', + 'x-pack/test/**/screenshots/failure/*.png', + 'x-pack/test/**/screenshots/diff/*.png', 'x-pack/test/functional/failure_debug/html/*.html', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', ] @@ -140,6 +197,12 @@ def withGcsArtifactUpload(workerName, closure) { ARTIFACT_PATTERNS.each { pattern -> uploadGcsArtifact(uploadPrefix, pattern) } + + dir(env.WORKSPACE) { + ARTIFACT_PATTERNS.each { pattern -> + uploadGcsArtifact(uploadPrefix, "parallel/*/kibana/${pattern}") + } + } } } }) @@ -152,6 +215,10 @@ def withGcsArtifactUpload(workerName, closure) { def publishJunit() { junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) + + dir(env.WORKSPACE) { + junit(testResults: 'parallel/*/kibana/target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) + } } def sendMail() { @@ -217,26 +284,36 @@ def doSetup() { } } -def buildOss() { +def buildOss(maxWorkers = '') { notifyOnError { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + } } } -def buildXpack() { +def buildXpack(maxWorkers = '') { notifyOnError { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + } } } def runErrorReporter() { + return runErrorReporter([pwd()]) +} + +def runErrorReporter(workspaces) { def status = buildUtils.getBuildStatus() def dryRun = status != "ABORTED" ? "" : "--no-github-update" + def globs = workspaces.collect { "'${it}/target/junit/**/*.xml'" }.join(" ") + bash( """ source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests ${dryRun} target/junit/**/*.xml + node scripts/report_failed_tests ${dryRun} ${globs} """, "Report failed tests, if necessary" ) @@ -275,6 +352,102 @@ def call(Map params = [:], Closure closure) { } } +// Creates a task queue using withTaskQueue, and copies the bootstrapped kibana repo into each process's workspace +// Note that node_modules are mostly symlinked to save time/space. See test/scripts/jenkins_setup_parallel_workspace.sh +def withCiTaskQueue(Map options = [:], Closure closure) { + def setupClosure = { + // This can't use runbld, because it expects the source to be there, which isn't yet + bash("${env.WORKSPACE}/kibana/test/scripts/jenkins_setup_parallel_workspace.sh", "Set up duplicate workspace for parallel process") + } + + def config = [parallel: 24, setup: setupClosure] + options + + withTaskQueue(config) { + closure.call() + } +} + +def scriptTask(description, script) { + return { + withFunctionalTestEnv { + notifyOnError { + runbld(script, description) + } + } + } +} + +def scriptTaskDocker(description, script) { + return { + withDocker(scriptTask(description, script)) + } +} + +def buildDocker() { + sh( + script: """ + cp /usr/local/bin/runbld .ci/ + cp /usr/local/bin/bash_standard_lib.sh .ci/ + cd .ci + docker build -t kibana-ci -f ./Dockerfile . + """, + label: 'Build CI Docker image' + ) +} + +def withDocker(Closure closure) { + docker + .image('kibana-ci') + .inside( + "-v /etc/runbld:/etc/runbld:ro -v '${env.JENKINS_HOME}:${env.JENKINS_HOME}' -v '/dev/shm/workspace:/dev/shm/workspace' --shm-size 2GB --cpus 4", + closure + ) +} + +def buildOssPlugins() { + runbld('./test/scripts/jenkins_build_plugins.sh', 'Build OSS Plugins') +} + +def buildXpackPlugins() { + runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') +} + +def withTasks(Map params = [worker: [:]], Closure closure) { + catchErrors { + def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:]) + + workers.ci(config) { + withCiTaskQueue(parallel: 24) { + parallel([ + docker: { + retry(2) { + buildDocker() + } + }, + + // There are integration tests etc that require the plugins to be built first, so let's go ahead and build them before set up the parallel workspaces + ossPlugins: { buildOssPlugins() }, + xpackPlugins: { buildXpackPlugins() }, + ]) + + catchErrors { + closure() + } + } + } + } +} + +def allCiTasks() { + withTasks { + tasks.check() + tasks.lint() + tasks.test() + tasks.functionalOss() + tasks.functionalXpack() + } +} + def pipelineLibraryTests() { whenChanged(['vars/', '.ci/pipeline-library/']) { workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) { @@ -285,5 +458,4 @@ def pipelineLibraryTests() { } } - return this diff --git a/vars/task.groovy b/vars/task.groovy new file mode 100644 index 000000000000..0c07b519b6fe --- /dev/null +++ b/vars/task.groovy @@ -0,0 +1,5 @@ +def call(Closure closure) { + withTaskQueue.addTask(closure) +} + +return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy new file mode 100644 index 000000000000..52641ce31f0b --- /dev/null +++ b/vars/tasks.groovy @@ -0,0 +1,119 @@ +def call(List closures) { + withTaskQueue.addTasks(closures) +} + +def check() { + tasks([ + kibanaPipeline.scriptTask('Check Telemetry Schema', 'test/scripts/checks/telemetry.sh'), + kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'), + kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'), + kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'), + kibanaPipeline.scriptTask('Check i18n', 'test/scripts/checks/i18n.sh'), + kibanaPipeline.scriptTask('Check File Casing', 'test/scripts/checks/file_casing.sh'), + kibanaPipeline.scriptTask('Check Lockfile Symlinks', 'test/scripts/checks/lock_file_symlinks.sh'), + kibanaPipeline.scriptTask('Check Licenses', 'test/scripts/checks/licenses.sh'), + kibanaPipeline.scriptTask('Verify Dependency Versions', 'test/scripts/checks/verify_dependency_versions.sh'), + kibanaPipeline.scriptTask('Verify NOTICE', 'test/scripts/checks/verify_notice.sh'), + kibanaPipeline.scriptTask('Test Projects', 'test/scripts/checks/test_projects.sh'), + kibanaPipeline.scriptTask('Test Hardening', 'test/scripts/checks/test_hardening.sh'), + ]) +} + +def lint() { + tasks([ + kibanaPipeline.scriptTask('Lint: eslint', 'test/scripts/lint/eslint.sh'), + kibanaPipeline.scriptTask('Lint: sasslint', 'test/scripts/lint/sasslint.sh'), + ]) +} + +def test() { + tasks([ + // These 2 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here + kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh'), + kibanaPipeline.scriptTaskDocker('Mocha Tests', 'test/scripts/test/mocha.sh'), + + kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), + kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), + kibanaPipeline.scriptTask('@elastic/safer-lodash-set Tests', 'test/scripts/test/safer_lodash_set.sh'), + kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'), + kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'), + kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), + ]) +} + +def functionalOss(Map params = [:]) { + def config = params ?: [ciGroups: true, firefox: true, accessibility: true, pluginFunctional: true, visualRegression: false] + + task { + kibanaPipeline.buildOss(6) + + if (config.ciGroups) { + def ciGroups = 1..12 + tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it) }) + } + + if (config.firefox) { + task(kibanaPipeline.functionalTestProcess('oss-firefox', './test/scripts/jenkins_firefox_smoke.sh')) + } + + if (config.accessibility) { + task(kibanaPipeline.functionalTestProcess('oss-accessibility', './test/scripts/jenkins_accessibility.sh')) + } + + if (config.pluginFunctional) { + task(kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh')) + } + + if (config.visualRegression) { + task(kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')) + } + } +} + +def functionalXpack(Map params = [:]) { + def config = params ?: [ + ciGroups: true, + firefox: true, + accessibility: true, + pluginFunctional: true, + savedObjectsFieldMetrics:true, + pageLoadMetrics: false, + visualRegression: false, + ] + + task { + kibanaPipeline.buildXpack(10) + + if (config.ciGroups) { + def ciGroups = 1..10 + tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it) }) + } + + if (config.firefox) { + task(kibanaPipeline.functionalTestProcess('xpack-firefox', './test/scripts/jenkins_xpack_firefox_smoke.sh')) + } + + if (config.accessibility) { + task(kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh')) + } + + if (config.visualRegression) { + task(kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')) + } + + if (config.savedObjectsFieldMetrics) { + task(kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh')) + } + + whenChanged([ + 'x-pack/plugins/security_solution/', + 'x-pack/test/security_solution_cypress/', + 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', + 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx', + ]) { + task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) + } + } +} + +return this diff --git a/vars/withTaskQueue.groovy b/vars/withTaskQueue.groovy new file mode 100644 index 000000000000..8132d6264744 --- /dev/null +++ b/vars/withTaskQueue.groovy @@ -0,0 +1,154 @@ +import groovy.transform.Field + +public static @Field TASK_QUEUES = [:] +public static @Field TASK_QUEUES_COUNTER = 0 + +/** + withTaskQueue creates a queue of "tasks" (just plain closures to execute), and executes them with your desired level of concurrency. + This way, you can define, for example, 40 things that need to execute, then only allow 10 of them to execute at once. + + Each "process" will execute in a separate, unique, empty directory. + If you want each process to have a bootstrapped kibana repo, check out kibanaPipeline.withCiTaskQueue + + Using the queue currently requires an agent/worker. + + Usage: + + withTaskQueue(parallel: 10) { + task { print "This is a task" } + + // This is the same as calling task() multiple times + tasks([ { print "Another task" }, { print "And another task" } ]) + + // Tasks can queue up subsequent tasks + task { + buildThing() + task { print "I depend on buildThing()" } + } + } + + You can also define a setup task that each process should execute one time before executing tasks: + withTaskQueue(parallel: 10, setup: { sh "my-setup-scrupt.sh" }) { + ... + } + +*/ +def call(Map options = [:], Closure closure) { + def config = [ parallel: 10 ] + options + def counter = ++TASK_QUEUES_COUNTER + + // We're basically abusing withEnv() to create a "scope" for all steps inside of a withTaskQueue block + // This way, we could have multiple task queue instances in the same pipeline + withEnv(["TASK_QUEUE_ID=${counter}"]) { + withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID] = [ + tasks: [], + tmpFile: sh(script: 'mktemp', returnStdout: true).trim() + ] + + closure.call() + + def processesExecuting = 0 + def processes = [:] + def iterationId = 0 + + for(def i = 1; i <= config.parallel; i++) { + def j = i + processes["task-queue-process-${j}"] = { + catchErrors { + withEnv([ + "TASK_QUEUE_PROCESS_ID=${j}", + "TASK_QUEUE_ITERATION_ID=${++iterationId}" + ]) { + dir("${WORKSPACE}/parallel/${j}/kibana") { + if (config.setup) { + config.setup.call(j) + } + + def isDone = false + while(!isDone) { // TODO some kind of timeout? + catchErrors { + if (!getTasks().isEmpty()) { + processesExecuting++ + catchErrors { + def task + try { + task = getTasks().pop() + } catch (java.util.NoSuchElementException ex) { + return + } + + task.call() + } + processesExecuting-- + // If a task finishes, and no new tasks were queued up, and nothing else is executing + // Then all of the processes should wake up and exit + if (processesExecuting < 1 && getTasks().isEmpty()) { + taskNotify() + } + return + } + + if (processesExecuting > 0) { + taskSleep() + return + } + + // Queue is empty, no processes are executing + isDone = true + } + } + } + } + } + } + } + parallel(processes) + } +} + +// If we sleep in a loop using Groovy code, Pipeline Steps is flooded with Sleep steps +// So, instead, we just watch a file and `touch` it whenever something happens that could modify the queue +// There's a 20 minute timeout just in case something goes wrong, +// in which case this method will get called again if the process is actually supposed to be waiting. +def taskSleep() { + sh(script: """#!/bin/bash + TIMESTAMP=\$(date '+%s' -d "0 seconds ago") + for (( i=1; i<=240; i++ )) + do + if [ "\$(stat -c %Y '${getTmpFile()}')" -ge "\$TIMESTAMP" ] + then + break + else + sleep 5 + if [[ \$i == 240 ]]; then + echo "Waited for new tasks for 20 minutes, exiting in case something went wrong" + fi + fi + done + """, label: "Waiting for new tasks...") +} + +// Used to let the task queue processes know that either a new task has been queued up, or work is complete +def taskNotify() { + sh "touch '${getTmpFile()}'" +} + +def getTasks() { + return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tasks +} + +def getTmpFile() { + return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tmpFile +} + +def addTask(Closure closure) { + getTasks() << closure + taskNotify() +} + +def addTasks(List closures) { + closures.reverse().each { + getTasks() << it + } + taskNotify() +} diff --git a/vars/workers.groovy b/vars/workers.groovy index f5a28c97c681..e582e996a78b 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -13,6 +13,8 @@ def label(size) { return 'docker && tests-l' case 'xl': return 'docker && tests-xl' + case 'xl-highmem': + return 'docker && tests-xl-highmem' case 'xxl': return 'docker && tests-xxl' } @@ -55,6 +57,11 @@ def base(Map params, Closure closure) { } } + sh( + script: "mkdir -p ${env.WORKSPACE}/tmp", + label: "Create custom temp directory" + ) + def checkoutInfo = [:] if (config.scm) { @@ -89,6 +96,7 @@ def base(Map params, Closure closure) { "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", "TEST_BROWSER_HEADLESS=1", "GIT_BRANCH=${checkoutInfo.branch}", + "TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it ]) { withCredentials([ string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), @@ -169,7 +177,9 @@ def parallelProcesses(Map params) { sleep(delay) } - processClosure(processNumber) + withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) { + processClosure() + } } } diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index ce5c1fe8500f..b25e33400df5 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -41,7 +41,7 @@ beforeEach(() => { }; }); -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; @@ -203,7 +203,9 @@ describe('isActionTypeEnabled', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'basic', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { @@ -258,7 +260,9 @@ describe('ensureActionTypeEnabled', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'basic', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 1f7409fedd2c..4015381ff950 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -8,9 +8,15 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; -import { ActionType, PreConfiguredAction } from './types'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; +import { + ActionType, + PreConfiguredAction, + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, +} from './types'; export interface ActionTypeRegistryOpts { taskManager: TaskManagerSetupContract; @@ -77,7 +83,12 @@ export class ActionTypeRegistry { /** * Registers an action type to the action type registry */ - public register(actionType: ActionType) { + public register< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >(actionType: ActionType) { if (this.has(actionType.id)) { throw new Error( i18n.translate( @@ -91,7 +102,7 @@ export class ActionTypeRegistry { ) ); } - this.actionTypes.set(actionType.id, { ...actionType }); + this.actionTypes.set(actionType.id, { ...actionType } as ActionType); this.taskManager.registerTaskDefinitions({ [`actions:${actionType.id}`]: { title: actionType.name, @@ -112,7 +123,12 @@ export class ActionTypeRegistry { /** * Returns an action type, throws if not registered */ - public get(id: string): ActionType { + public get< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >(id: string): ActionType { if (!this.has(id)) { throw Boom.badRequest( i18n.translate('xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage', { @@ -123,7 +139,7 @@ export class ActionTypeRegistry { }) ); } - return this.actionTypes.get(id)!; + return this.actionTypes.get(id)! as ActionType; } /** diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 90b989ac3b52..16a5a59882dd 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -39,7 +39,7 @@ let actionsClient: ActionsClient; let mockedLicenseState: jest.Mocked; let actionTypeRegistry: ActionTypeRegistry; let actionTypeRegistryParams: ActionTypeRegistryOpts; -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 6744a8d11162..d46ad3e2e242 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -298,7 +298,7 @@ export class ActionsClient { public async execute({ actionId, params, - }: Omit): Promise { + }: Omit): Promise> { await this.authorization.ensureAuthorized('execute'); return this.actionExecutor.execute({ actionId, params, request: this.request }); } 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 676a4776d005..82dedb09c429 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 @@ -10,6 +10,10 @@ import { schema } from '@kbn/config-schema'; import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; import { ExecutorParamsSchema } from './schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from './types'; import { CreateExternalServiceArgs, @@ -23,6 +27,7 @@ import { TransformFieldsArgs, Comment, ExecutorSubActionPushParams, + PushToServiceResponse, } from './types'; import { transformers } from './transformers'; @@ -63,14 +68,17 @@ export const createConnectorExecutor = ({ api, createExternalService, }: CreateExternalServiceBasicArgs) => async ( - execOptions: ActionTypeExecutorOptions -): Promise => { + execOptions: ActionTypeExecutorOptions< + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, + ExecutorParams + > +): Promise> => { const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params as ExecutorParams; + const { subAction, subActionParams } = params; let data = {}; - const res: Pick & - Pick = { + const res: ActionTypeExecutorResult = { status: 'ok', actionId, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 1a24622e1cab..195f6db538ae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -10,7 +10,6 @@ jest.mock('./lib/send_email', () => ({ import { Logger } from '../../../../../src/core/server'; -import { ActionType, ActionTypeExecutorOptions } from '../types'; import { actionsConfigMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; @@ -21,6 +20,8 @@ import { ActionTypeConfigType, ActionTypeSecretsType, getActionType, + EmailActionType, + EmailActionTypeExecutorOptions, } from './email'; const sendEmailMock = sendEmail as jest.Mock; @@ -29,13 +30,17 @@ const ACTION_TYPE_ID = '.email'; const services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: EmailActionType; let mockedLogger: jest.Mocked; beforeEach(() => { jest.resetAllMocks(); const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); }); describe('actionTypeRegistry.get() works', () => { @@ -242,7 +247,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, @@ -306,7 +311,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, @@ -363,7 +368,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 7ddb123a4d78..a51a0432a01e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -15,6 +15,18 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +export type EmailActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -30,10 +42,9 @@ const ConfigSchema = schema.object(ConfigSchemaProps); function validateConfig( configurationUtilities: ActionsConfigurationUtilities, - configObject: unknown + configObject: ActionTypeConfigType ): string | void { - // avoids circular reference ... - const config = configObject as ActionTypeConfigType; + const config = configObject; // Make sure service is set, or if not, both host/port must be set. // If service is set, host/port are ignored, when the email is sent. @@ -113,7 +124,7 @@ interface GetActionTypeParams { } // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { +export function getActionType(params: GetActionTypeParams): EmailActionType { const { logger, configurationUtilities } = params; return { id: '.email', @@ -136,12 +147,12 @@ export function getActionType(params: GetActionTypeParams): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: EmailActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const secrets = execOptions.secrets; + const params = execOptions.params; const transport: Transport = {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index be60f4c2f28a..7a0e24521a1c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -8,21 +8,25 @@ jest.mock('./lib/send_email', () => ({ sendEmail: jest.fn(), })); -import { ActionType, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; -import { ActionParamsType, ActionTypeConfigType } from './es_index'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ActionTypeConfigType, + ESIndexActionType, + ESIndexActionTypeExecutorOptions, +} from './es_index'; const ACTION_TYPE_ID = '.index'; const services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: ESIndexActionType; beforeAll(() => { const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get(ACTION_TYPE_ID); }); beforeEach(() => { @@ -144,12 +148,12 @@ describe('params validation', () => { describe('execute()', () => { test('ensure parameters are as expected', async () => { const secrets = {}; - let config: Partial; + let config: ActionTypeConfigType; let params: ActionParamsType; - let executorOptions: ActionTypeExecutorOptions; + let executorOptions: ESIndexActionTypeExecutorOptions; // minimal params - config = { index: 'index-value', refresh: false }; + config = { index: 'index-value', refresh: false, executionTimeField: null }; params = { documents: [{ jim: 'bob' }], }; @@ -215,7 +219,7 @@ describe('execute()', () => { `); // minimal params - config = { index: 'index-value', executionTimeField: undefined, refresh: false }; + config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ jim: 'bob' }], }; @@ -245,7 +249,7 @@ describe('execute()', () => { `); // multiple documents - config = { index: 'index-value', executionTimeField: undefined, refresh: false }; + config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ a: 1 }, { b: 2 }], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 899684367d52..53bf75651b1e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -11,6 +11,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; +export type ESIndexActionType = ActionType; +export type ESIndexActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + {}, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -33,7 +40,7 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType({ logger }: { logger: Logger }): ActionType { +export function getActionType({ logger }: { logger: Logger }): ESIndexActionType { return { id: '.index', minimumLicenseRequired: 'basic', @@ -52,11 +59,11 @@ export function getActionType({ logger }: { logger: Logger }): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ESIndexActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const params = execOptions.params; const services = execOptions.services; const index = config.index; diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index b1ed3728edfa..c379c05ee88e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -8,14 +8,21 @@ jest.mock('./lib/post_pagerduty', () => ({ postPagerduty: jest.fn(), })); -import { getActionType } from './pagerduty'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; +import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { postPagerduty } from './lib/post_pagerduty'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ActionTypeConfigType, + ActionTypeSecretsType, + getActionType, + PagerDutyActionType, + PagerDutyActionTypeExecutorOptions, +} from './pagerduty'; const postPagerdutyMock = postPagerduty as jest.Mock; @@ -23,12 +30,16 @@ const ACTION_TYPE_ID = '.pagerduty'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: PagerDutyActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); mockedLogger = logger; }); @@ -167,7 +178,7 @@ describe('execute()', () => { test('should succeed with minimal valid params', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -175,7 +186,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -219,7 +230,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'trigger', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -236,7 +247,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -284,7 +295,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'acknowledge', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -301,7 +312,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -340,7 +351,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'resolve', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -357,7 +368,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -390,7 +401,7 @@ describe('execute()', () => { test('should fail when sendPagerdury throws', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -398,7 +409,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -418,7 +429,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 429', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -426,7 +437,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -446,7 +457,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 501', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -454,7 +465,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -474,7 +485,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 418', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -482,7 +493,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 0c8802060164..b76e57419bc5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -16,6 +16,18 @@ import { ActionsConfigurationUtilities } from '../actions_config'; // https://v2.developer.pagerduty.com/docs/events-api-v2 const PAGER_DUTY_API_URL = 'https://events.pagerduty.com/v2/enqueue'; +export type PagerDutyActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type PagerDutyActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -100,7 +112,7 @@ export function getActionType({ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { +}): PagerDutyActionType { return { id: '.pagerduty', minimumLicenseRequired: 'gold', @@ -142,12 +154,12 @@ function getPagerDutyApiUrl(config: ActionTypeConfigType): string { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: PagerDutyActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const secrets = execOptions.secrets; + const params = execOptions.params; const services = execOptions.services; const apiUrl = getPagerDutyApiUrl(config); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index d5a9c0cc1ccd..e4828f33765c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../types'; import { validateParams } from '../lib'; import { Logger } from '../../../../../src/core/server'; import { createActionTypeRegistry } from './index.test'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ServerLogActionType, + ServerLogActionTypeExecutorOptions, +} from './server_log'; const ACTION_TYPE_ID = '.server-log'; -let actionType: ActionType; +let actionType: ServerLogActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get<{}, {}, ActionParamsType>(ACTION_TYPE_ID); mockedLogger = logger; expect(actionType).toBeTruthy(); }); @@ -88,13 +92,14 @@ describe('validateParams()', () => { describe('execute()', () => { test('calls the executor with proper params', async () => { const actionId = 'some-id'; - await actionType.executor({ + const executorOptions: ServerLogActionTypeExecutorOptions = { actionId, services: actionsMock.createServices(), params: { message: 'message text here', level: 'info' }, config: {}, secrets: {}, - }); + }; + await actionType.executor(executorOptions); expect(mockedLogger.info).toHaveBeenCalledWith('Server log: message text here'); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index bf8a3d8032cc..490764fb16bf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -12,6 +12,13 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { withoutControlCharacters } from './lib/string_utils'; +export type ServerLogActionType = ActionType<{}, {}, ActionParamsType>; +export type ServerLogActionTypeExecutorOptions = ActionTypeExecutorOptions< + {}, + {}, + ActionParamsType +>; + // params definition export type ActionParamsType = TypeOf; @@ -32,7 +39,7 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType({ logger }: { logger: Logger }): ActionType { +export function getActionType({ logger }: { logger: Logger }): ServerLogActionType { return { id: '.server-log', minimumLicenseRequired: 'basic', @@ -50,10 +57,10 @@ export function getActionType({ logger }: { logger: Logger }): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ServerLogActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const params = execOptions.params as ActionParamsType; + const params = execOptions.params; const sanitizedMessage = withoutControlCharacters(params.message); try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index e62ca465f30f..109008b8fc9f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -17,9 +17,14 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { createExternalService } from './service'; import { api } from './api'; -import { ExecutorParams, ExecutorSubActionPushParams } from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; +import { + ExecutorParams, + ExecutorSubActionPushParams, + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, +} from './types'; // TODO: to remove, need to support Case import { buildMap, mapParams } from '../case/utils'; @@ -31,7 +36,14 @@ interface GetActionTypeParams { } // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { +export function getActionType( + params: GetActionTypeParams +): ActionType< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse | {} +> { const { logger, configurationUtilities } = params; return { id: '.servicenow', @@ -54,10 +66,14 @@ export function getActionType(params: GetActionTypeParams): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ActionTypeExecutorOptions< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams + > +): Promise> { const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params as ExecutorParams; + const { subAction, subActionParams } = params; let data: PushToServiceResponse | null = null; const externalService = createExternalService({ @@ -81,9 +97,8 @@ async function executor( const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; const { comments, externalId, ...restParams } = pushToServiceParams; - const mapping = config.incidentConfiguration - ? buildMap(config.incidentConfiguration.mapping) - : null; + const incidentConfiguration = config.incidentConfiguration; + const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; const externalObject = config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index d1a739c2304f..6d4176067c3b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,14 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ActionType, - Services, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, -} from '../types'; +import { Services, ActionTypeExecutorResult } from '../types'; import { validateParams, validateSecrets } from '../lib'; -import { getActionType } from './slack'; +import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; @@ -19,11 +14,13 @@ const ACTION_TYPE_ID = '.slack'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: SlackActionType; beforeAll(() => { actionType = getActionType({ - async executor() {}, + async executor(options) { + return { status: 'ok', actionId: options.actionId }; + }, configurationUtilities: actionsConfigMock.create(), }); }); @@ -119,7 +116,7 @@ describe('validateActionTypeSecrets()', () => { describe('execute()', () => { beforeAll(() => { - async function mockSlackExecutor(options: ActionTypeExecutorOptions) { + async function mockSlackExecutor(options: SlackActionTypeExecutorOptions) { const { params } = options; const { message } = params; if (message == null) throw new Error('message property required in parameter'); @@ -134,7 +131,7 @@ describe('execute()', () => { text: `slack mockExecutor success: ${message}`, actionId: '', status: 'ok', - } as ActionTypeExecutorResult; + } as ActionTypeExecutorResult; } actionType = getActionType({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 55c373f14cd6..209582585256 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -21,6 +21,13 @@ import { } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; +export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< + {}, + ActionTypeSecretsType, + ActionParamsType +>; + // secrets definition export type ActionTypeSecretsType = TypeOf; @@ -46,8 +53,8 @@ export function getActionType({ executor = slackExecutor, }: { configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { + executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; +}): SlackActionType { return { id: '.slack', minimumLicenseRequired: 'gold', @@ -92,11 +99,11 @@ function valdiateActionTypeConfig( // action executor async function slackExecutor( - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: SlackActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const secrets = execOptions.secrets; + const params = execOptions.params; let result: IncomingWebhookResult; const { webhookUrl } = secrets; @@ -156,18 +163,21 @@ async function slackExecutor( return successResult(actionId, result); } -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } -function errorResult(actionId: string, message: string): ActionTypeExecutorResult { +function errorResult(actionId: string, message: string): ActionTypeExecutorResult { return { status: 'error', message, actionId, }; } -function serviceErrorResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function serviceErrorResult( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.slack.errorPostingErrorMessage', { defaultMessage: 'error posting slack message', }); @@ -179,7 +189,7 @@ function serviceErrorResult(actionId: string, serviceMessage: string): ActionTyp }; } -function retryResult(actionId: string, message: string): ActionTypeExecutorResult { +function retryResult(actionId: string, message: string): ActionTypeExecutorResult { const errMessage = i18n.translate( 'xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage', { @@ -198,7 +208,7 @@ function retryResultSeconds( actionId: string, message: string, retryAfter: number -): ActionTypeExecutorResult { +): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; const retry = new Date(retryEpoch); const retryString = retry.toISOString(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 53b17f58d6e1..26dd8a1a1402 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -8,14 +8,21 @@ jest.mock('axios', () => ({ request: jest.fn(), })); -import { getActionType } from './webhook'; -import { ActionType, Services } from '../types'; +import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { actionsConfigMock } from '../actions_config.mock'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsMock } from '../mocks'; import axios from 'axios'; +import { + ActionParamsType, + ActionTypeConfigType, + ActionTypeSecretsType, + getActionType, + WebhookActionType, + WebhookMethods, +} from './webhook'; const axiosRequestMock = axios.request as jest.Mock; @@ -23,12 +30,16 @@ const ACTION_TYPE_ID = '.webhook'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: WebhookActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); mockedLogger = logger; }); @@ -235,16 +246,17 @@ describe('execute()', () => { }); test('execute with username/password sends request with basic auth', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + }; await actionType.executor({ actionId: 'some-id', services, - config: { - url: 'https://abc.def/my-webhook', - method: 'post', - headers: { - aheader: 'a value', - }, - }, + config, secrets: { user: 'abc', password: '123' }, params: { body: 'some data' }, }); @@ -266,17 +278,19 @@ describe('execute()', () => { }); test('execute without username/password sends request without basic auth', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + }; + const secrets: ActionTypeSecretsType = { user: null, password: null }; await actionType.executor({ actionId: 'some-id', services, - config: { - url: 'https://abc.def/my-webhook', - method: 'post', - headers: { - aheader: 'a value', - }, - }, - secrets: {}, + config, + secrets, params: { body: 'some data' }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 0b8b27b27892..be75742fa882 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -17,11 +17,23 @@ import { ActionsConfigurationUtilities } from '../actions_config'; import { Logger } from '../../../../../src/core/server'; // config definition -enum WebhookMethods { +export enum WebhookMethods { POST = 'post', PUT = 'put', } +export type WebhookActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type WebhookActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + const HeadersSchema = schema.recordOf(schema.string(), schema.string()); const configSchemaProps = { url: schema.string(), @@ -31,7 +43,7 @@ const configSchemaProps = { headers: nullableType(HeadersSchema), }; const ConfigSchema = schema.object(configSchemaProps); -type ActionTypeConfigType = TypeOf; +export type ActionTypeConfigType = TypeOf; // secrets definition export type ActionTypeSecretsType = TypeOf; @@ -51,7 +63,7 @@ const SecretsSchema = schema.object(secretSchemaProps, { }); // params definition -type ActionParamsType = TypeOf; +export type ActionParamsType = TypeOf; const ParamsSchema = schema.object({ body: schema.maybe(schema.string()), }); @@ -63,7 +75,7 @@ export function getActionType({ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { +}): WebhookActionType { return { id: '.webhook', minimumLicenseRequired: 'gold', @@ -112,13 +124,13 @@ function validateActionTypeConfig( // action executor export async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: WebhookActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const { method, url, headers = {} } = execOptions.config as ActionTypeConfigType; - const { body: data } = execOptions.params as ActionParamsType; + const { method, url, headers = {} } = execOptions.config; + const { body: data } = execOptions.params; - const secrets: ActionTypeSecretsType = execOptions.secrets as ActionTypeSecretsType; + const secrets: ActionTypeSecretsType = execOptions.secrets; const basicAuth = isString(secrets.user) && isString(secrets.password) ? { auth: { username: secrets.user, password: secrets.password } } @@ -172,11 +184,14 @@ export async function executor( } // Action Executor Result w/ internationalisation -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } -function errorResultInvalid(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function errorResultInvalid( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.webhook.invalidResponseErrorMessage', { defaultMessage: 'error calling webhook, invalid response', }); @@ -188,7 +203,7 @@ function errorResultInvalid(actionId: string, serviceMessage: string): ActionTyp }; } -function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { +function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.webhook.unreachableErrorMessage', { defaultMessage: 'error calling webhook, unexpected error', }); @@ -199,7 +214,7 @@ function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult }; } -function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { const errMessage = i18n.translate( 'xpack.actions.builtin.webhook.invalidResponseRetryLaterErrorMessage', { @@ -220,7 +235,7 @@ function retryResultSeconds( serviceMessage: string, retryAfter: number -): ActionTypeExecutorResult { +): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; const retry = new Date(retryEpoch); const retryString = retry.toISOString(); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0e63cc8f5956..bce06c829b1b 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -59,7 +59,7 @@ export class ActionExecutor { actionId, params, request, - }: ExecuteOptions): Promise { + }: ExecuteOptions): Promise> { if (!this.isInitialized) { throw new Error('ActionExecutor not initialized'); } @@ -125,7 +125,7 @@ export class ActionExecutor { }; eventLogger.startTiming(event); - let rawResult: ActionTypeExecutorResult | null | undefined | void; + let rawResult: ActionTypeExecutorResult; try { rawResult = await actionType.executor({ actionId, @@ -173,7 +173,7 @@ export class ActionExecutor { } } -function actionErrorToMessage(result: ActionTypeExecutorResult): string { +function actionErrorToMessage(result: ActionTypeExecutorResult): string { let message = result.message || 'unknown error running action'; if (result.serviceMessage) { diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index 0a474ec3ae3e..32c3c54faf00 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -59,7 +59,9 @@ describe('isLicenseValidForActionType', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'gold', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { @@ -120,7 +122,9 @@ describe('ensureLicenseForActionType()', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'gold', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 9204c41b9288..10a8501e856d 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -94,7 +94,7 @@ export class TaskRunnerFactory { }, } as unknown) as KibanaRequest; - let executorResult: ActionTypeExecutorResult; + let executorResult: ActionTypeExecutorResult; try { executorResult = await actionExecutor.execute({ params, diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index 03ae7a9b35a8..10c688c075ea 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { ActionType, ExecutorType } from '../types'; -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.ts index 021c460f4c81..50231f1c9a3a 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.ts @@ -5,24 +5,44 @@ */ import Boom from 'boom'; -import { ActionType } from '../types'; +import { ActionType, ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; -export function validateParams(actionType: ActionType, value: unknown) { +export function validateParams< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'params', value); } -export function validateConfig(actionType: ActionType, value: unknown) { +export function validateConfig< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'config', value); } -export function validateSecrets(actionType: ActionType, value: unknown) { +export function validateSecrets< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'secrets', value); } type ValidKeys = 'params' | 'config' | 'secrets'; -function validateWithSchema( - actionType: ActionType, +function validateWithSchema< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>( + actionType: ActionType, key: ValidKeys, value: unknown ): Record { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index ac4b332e7fd7..ca93e88d0120 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -125,7 +125,9 @@ describe('Actions Plugin', () => { id: 'test', name: 'test', minimumLicenseRequired: 'basic', - async executor() {}, + async executor(options) { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(async () => { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 5b8b25d02658..54d137cc0f61 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -33,13 +33,20 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { SecurityPluginSetup } from '../../security/server'; import { ActionsConfig } from './config'; -import { Services, ActionType, PreConfiguredAction } from './types'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; import { createExecutionEnqueuerFunction } from './create_execute_function'; import { registerBuiltInActionTypes } from './builtin_action_types'; import { registerActionsUsageCollector } from './usage'; +import { + Services, + ActionType, + PreConfiguredAction, + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, +} from './types'; import { getActionsConfigurationUtilities } from './actions_config'; @@ -70,7 +77,13 @@ export const EVENT_LOG_ACTIONS = { }; export interface PluginSetupContract { - registerType: (actionType: ActionType) => void; + registerType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams + >( + actionType: ActionType + ): void; } export interface PluginStartContract { @@ -219,7 +232,13 @@ export class ActionsPlugin implements Plugin, Plugi executeActionRoute(router, this.licenseState); return { - registerType: (actionType: ActionType) => { + registerType: < + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams + >( + actionType: ActionType + ) => { if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); } diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 38fca656bef5..b668e3460828 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -71,7 +71,9 @@ describe('executeActionRoute', () => { const router = httpServiceMock.createRouter(); const actionsClient = actionsClientMock.create(); - actionsClient.execute.mockResolvedValueOnce((null as unknown) as ActionTypeExecutorResult); + actionsClient.execute.mockResolvedValueOnce( + (null as unknown) as ActionTypeExecutorResult + ); const [context, req, res] = mockHandlerArguments( { actionsClient }, diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 0d49d9a3a256..f15a11710621 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -48,7 +48,7 @@ export const executeActionRoute = (router: IRouter, licenseState: ILicenseState) const { params } = req.body; const { id } = req.params; try { - const body: ActionTypeExecutorResult = await actionsClient.execute({ + const body: ActionTypeExecutorResult = await actionsClient.execute({ params, actionId: id, }); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index a8e19e3ff2e7..ecec45ade046 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -21,6 +21,9 @@ export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; +export type ActionTypeConfig = Record; +export type ActionTypeSecrets = Record; +export type ActionTypeParams = Record; export interface Services { callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; @@ -49,32 +52,27 @@ export interface ActionsConfigType { } // the parameters passed to an action type executor function -export interface ActionTypeExecutorOptions { +export interface ActionTypeExecutorOptions { actionId: string; services: Services; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params: Record; + config: Config; + secrets: Secrets; + params: Params; } -export interface ActionResult { +export interface ActionResult { id: string; actionTypeId: string; name: string; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config?: Record; + config?: Config; isPreconfigured: boolean; } -export interface PreConfiguredAction extends ActionResult { - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; +export interface PreConfiguredAction< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets +> extends ActionResult { + secrets: Secrets; } export interface FindActionResult extends ActionResult { @@ -82,38 +80,45 @@ export interface FindActionResult extends ActionResult { } // the result returned from an action type executor function -export interface ActionTypeExecutorResult { +export interface ActionTypeExecutorResult { actionId: string; status: 'ok' | 'error'; message?: string; serviceMessage?: string; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: any; + data?: Data; retry?: null | boolean | Date; } // signature of the action type executor function -export type ExecutorType = ( - options: ActionTypeExecutorOptions -) => Promise; +export type ExecutorType = ( + options: ActionTypeExecutorOptions +) => Promise>; + +interface ValidatorType { + validate(value: unknown): Type; +} -interface ValidatorType { - validate(value: unknown): Record; +export interface ActionValidationService { + isWhitelistedHostname(hostname: string): boolean; + isWhitelistedUri(uri: string): boolean; } -export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; -export interface ActionType { +export interface ActionType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +> { id: string; name: string; maxAttempts?: number; minimumLicenseRequired: LicenseType; validate?: { - params?: ValidatorType; - config?: ValidatorType; - secrets?: ValidatorType; + params?: ValidatorType; + config?: ValidatorType; + secrets?: ValidatorType; }; - executor: ExecutorType; + executor: ExecutorType; } export interface RawAction extends SavedObjectAttributes { diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 36e33fba89fb..2434d898389d 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -64,21 +64,20 @@ describe('DatePicker', () => { }); beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); - it('should set default query params in the URL', () => { + it('sets default query params in the URL', () => { mountDatePicker(); expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshPaused=false&refreshInterval=10000', + search: 'rangeFrom=now-15m&rangeTo=now', }) ); }); - it('should add missing default value', () => { + it('adds missing default value', () => { mountDatePicker({ rangeTo: 'now', refreshInterval: 5000, @@ -86,13 +85,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000', }) ); }); - it('should not set default query params in the URL when values already defined', () => { + it('does not set default query params in the URL when values already defined', () => { mountDatePicker({ rangeFrom: 'now-1d', rangeTo: 'now', @@ -102,7 +100,7 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(0); }); - it('should update the URL when the date range changes', () => { + it('updates the URL when the date range changes', () => { const datePicker = mountDatePicker(); datePicker.find(EuiSuperDatePicker).props().onTimeChange({ start: 'updated-start', @@ -113,13 +111,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(2); expect(mockHistoryPush).toHaveBeenLastCalledWith( expect.objectContaining({ - search: - 'rangeFrom=updated-start&rangeTo=updated-end&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=updated-start&rangeTo=updated-end', }) ); }); - it('should auto-refresh when refreshPaused is false', async () => { + it('enables auto-refresh when refreshPaused is false', async () => { jest.useFakeTimers(); const wrapper = mountDatePicker({ refreshPaused: false, @@ -132,7 +129,7 @@ describe('DatePicker', () => { wrapper.unmount(); }); - it('should NOT auto-refresh when refreshPaused is true', async () => { + it('disables auto-refresh when refreshPaused is true', async () => { jest.useFakeTimers(); mountDatePicker({ refreshPaused: true, refreshInterval: 1000 }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 5201d80de5a1..403a8cad854c 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -14,11 +14,7 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { clearCache } from '../../../services/rest/callApi'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; -import { - TimePickerQuickRange, - TimePickerTimeDefaults, - TimePickerRefreshInterval, -} from './typings'; +import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings'; function removeUndefinedAndEmptyProps(obj: T): Partial { return pickBy(obj, (value) => value !== undefined && !isEmpty(String(value))); @@ -36,19 +32,9 @@ export function DatePicker() { UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS ); - const timePickerRefreshIntervalDefaults = core.uiSettings.get< - TimePickerRefreshInterval - >(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); - const DEFAULT_VALUES = { rangeFrom: timePickerTimeDefaults.from, rangeTo: timePickerTimeDefaults.to, - refreshPaused: timePickerRefreshIntervalDefaults.pause, - /* - * Must be replaced by timePickerRefreshIntervalDefaults.value when this issue is fixed. - * https://github.com/elastic/kibana/issues/70562 - */ - refreshInterval: 10000, }; const commonlyUsedRanges = timePickerQuickRanges.map( diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx index a80dcca4a107..0c209b0aca91 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx @@ -30,7 +30,10 @@ export function AnomalyDetectionSetupLink() { const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); return ( - + {ANOMALY_DETECTION_LINK_LABEL} diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index a433b0b50723..8214c081e6ce 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -6,7 +6,6 @@ import { EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { mean } from 'lodash'; import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; import { useChartsSync } from '../../../../hooks/useChartsSync'; @@ -79,7 +78,7 @@ export function ErroneousTransactionsRateChart() { { color: theme.euiColorVis7, data: [], - legendValue: tickFormatY(mean(errorRates.map((rate) => rate.y))), + legendValue: tickFormatY(data?.average), legendClickDisabled: true, title: i18n.translate('xpack.apm.errorRateChart.avgLabel', { defaultMessage: 'Avg.', diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 03d6ade7bea6..71e3386d821f 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -15,7 +15,7 @@ export const ComponentStrings = { }), getTitleText: () => i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), }, AdvancedFilter: { @@ -1308,7 +1308,7 @@ export const ComponentStrings = { }), getEmbedObjectMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), getFilterMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index b51a85edaa67..85ec7baf18c6 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import fs from 'fs'; import { ReactChildren } from 'react'; import path from 'path'; import moment from 'moment'; @@ -94,6 +95,12 @@ jest.mock('../shareable_runtime/components/rendered_element'); // @ts-expect-error RenderedElement.mockImplementation(() => 'RenderedElement'); +// Some of the code requires that this directory exists, but the tests don't actually require any css to be present +const cssDir = path.resolve(__dirname, '../../../../built_assets/css'); +if (!fs.existsSync(cssDir)) { + fs.mkdirSync(cssDir, { recursive: true }); +} + addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots diff --git a/x-pack/plugins/dashboard_enhanced/README.md b/x-pack/plugins/dashboard_enhanced/README.md index d9296ae15862..0aeb156a99f1 100644 --- a/x-pack/plugins/dashboard_enhanced/README.md +++ b/x-pack/plugins/dashboard_enhanced/README.md @@ -1 +1 @@ -# X-Pack part of Dashboard app +Contains the enhancements to the OSS dashboard app. \ No newline at end of file diff --git a/x-pack/plugins/dashboard_mode/README.md b/x-pack/plugins/dashboard_mode/README.md new file mode 100644 index 000000000000..4e244afb97fd --- /dev/null +++ b/x-pack/plugins/dashboard_mode/README.md @@ -0,0 +1 @@ +The deprecated dashboard only mode. \ No newline at end of file diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index f0baa84afca3..637af39339e2 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -8,7 +8,7 @@ "requiredPlugins": [ "data" ], - "optionalPlugins": ["kibanaReact", "kibanaUtils"], + "optionalPlugins": ["kibanaReact", "kibanaUtils", "usageCollection"], "server": true, "ui": true, "requiredBundles": ["kibanaReact", "kibanaUtils"] diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 9bd1ffddeaca..639f56d0cafc 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -52,8 +52,6 @@ describe('EnhancedSearchInterceptor', () => { trackLongQueryPopupShown: jest.fn(), trackLongQueryDialogDismissed: jest.fn(), trackLongQueryRunBeyondTimeout: jest.fn(), - trackError: jest.fn(), - trackSuccess: jest.fn(), }; searchInterceptor = new EnhancedSearchInterceptor( @@ -458,7 +456,6 @@ describe('EnhancedSearchInterceptor', () => { expect(next.mock.calls[1][0]).toStrictEqual(timedResponses[1].value); expect(error).not.toHaveBeenCalled(); expect(mockUsageCollector.trackLongQueryRunBeyondTimeout).toBeCalledTimes(1); - expect(mockUsageCollector.trackSuccess).toBeCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index d1ed41006524..927dc91f365b 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -89,9 +89,6 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { // If the response indicates it is complete, stop polling and complete the observable if (!response.is_running) { - if (this.deps.usageCollector && response.rawResponse) { - this.deps.usageCollector.trackSuccess(response.rawResponse.took); - } return EMPTY; } diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 9c3a0edf7e73..0e9731a41411 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -12,11 +12,17 @@ import { Logger, } from '../../../../src/core/server'; import { ES_SEARCH_STRATEGY } from '../../../../src/plugins/data/common'; -import { PluginSetup as DataPluginSetup } from '../../../../src/plugins/data/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, + usageProvider, +} from '../../../../src/plugins/data/server'; import { enhancedEsSearchStrategyProvider } from './search'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; interface SetupDependencies { data: DataPluginSetup; + usageCollection?: UsageCollectionSetup; } export class EnhancedDataServerPlugin implements Plugin { @@ -26,12 +32,15 @@ export class EnhancedDataServerPlugin implements Plugin, deps: SetupDependencies) { + const usage = deps.usageCollection ? usageProvider(core) : undefined; + deps.data.search.registerSearchStrategy( ES_SEARCH_STRATEGY, enhancedEsSearchStrategyProvider( this.initializerContext.config.legacy.globalConfig$, - this.logger + this.logger, + usage ) ); } diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index faa4f2ee499e..4fd1e889ba1a 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -113,4 +113,19 @@ describe('ES search strategy', () => { expect(method).toBe('POST'); expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); }); + + it('sets wait_for_completion_timeout and keep_alive in the request', async () => { + mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'foo-*', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); + const { query } = mockApiCaller.mock.calls[0][1]; + expect(query).toHaveProperty('wait_for_completion_timeout'); + expect(query).toHaveProperty('keep_alive'); + }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 358335a2a4d6..d2a8384b1f88 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -19,20 +19,34 @@ import { getDefaultSearchParams, getTotalLoaded, ISearchStrategy, + SearchUsage, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; import { shimHitsTotal } from './shim_hits_total'; +import { IEsSearchResponse } from '../../../../../src/plugins/data/common/search/es_search'; -export interface AsyncSearchResponse { +interface AsyncSearchResponse { id: string; is_partial: boolean; is_running: boolean; response: SearchResponse; } +interface EnhancedEsSearchResponse extends IEsSearchResponse { + is_partial: boolean; + is_running: boolean; +} + +function isEnhancedEsSearchResponse( + response: IEsSearchResponse +): response is EnhancedEsSearchResponse { + return response.hasOwnProperty('is_partial') && response.hasOwnProperty('is_running'); +} + export const enhancedEsSearchStrategyProvider = ( config$: Observable, - logger: Logger + logger: Logger, + usage?: SearchUsage ): ISearchStrategy => { const search = async ( context: RequestHandlerContext, @@ -45,9 +59,24 @@ export const enhancedEsSearchStrategyProvider = ( const defaultParams = getDefaultSearchParams(config); const params = { ...defaultParams, ...request.params }; - return request.indexType === 'rollup' - ? rollupSearch(caller, { ...request, params }, options) - : asyncSearch(caller, { ...request, params }, options); + try { + const response = + request.indexType === 'rollup' + ? await rollupSearch(caller, { ...request, params }, options) + : await asyncSearch(caller, { ...request, params }, options); + + if ( + usage && + (!isEnhancedEsSearchResponse(response) || (!response.is_partial && !response.is_running)) + ) { + usage.trackSuccess(response.rawResponse.took); + } + + return response; + } catch (e) { + if (usage) usage.trackError(); + throw e; + } }; const cancel = async (context: RequestHandlerContext, id: string) => { @@ -80,8 +109,15 @@ async function asyncSearch( const method = request.id ? 'GET' : 'POST'; const path = encodeURI(request.id ? `/_async_search/${request.id}` : `/${index}/_async_search`); - // Wait up to 1s for the response to return - const query = toSnakeCase({ waitForCompletionTimeout: '100ms', ...queryParams }); + // Only report partial results every 64 shards; this should be reduced when we actually display partial results + const batchedReduceSize = request.id ? undefined : 64; + + const query = toSnakeCase({ + waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return + keepAlive: '1m', // Extend the TTL for this search request by one minute + ...(batchedReduceSize && { batchedReduceSize }), + ...queryParams, + }); const { id, response, is_partial, is_running } = (await caller( 'transport.request', diff --git a/x-pack/plugins/discover_enhanced/README.md b/x-pack/plugins/discover_enhanced/README.md new file mode 100644 index 000000000000..08d0dbb9cdbe --- /dev/null +++ b/x-pack/plugins/discover_enhanced/README.md @@ -0,0 +1 @@ +Contains the enhancements to the OSS discover app. \ No newline at end of file diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index d5b50fce3871..9d28ef71a551 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -162,11 +162,10 @@ export const AlertPreview: React.FC = (props) => { {' '} @@ -187,11 +186,9 @@ export const AlertPreview: React.FC = (props) => { {showNoDataResults && previewResult.resultTotals.noData ? ( {previewResult.resultTotals.noData}, - plural: previewResult.resultTotals.noData !== 1 ? 's' : '', }} /> ) : null}{' '} diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx index e0e293b1cc3e..81f52f986cab 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx @@ -61,7 +61,7 @@ export const SubscriptionSplashContent: React.FC = () => { description = ( ); diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 2e06ee55189d..83fe23355335 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -176,7 +176,7 @@ export function SavedViewsToolbarControls(props: Props) { {currentView ? currentView.name : i18n.translate('xpack.infra.savedView.unknownView', { - defaultMessage: 'No view seleted', + defaultMessage: 'No view selected', })} diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index dec8eaae56f4..710011d3bdf3 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -68,6 +68,9 @@ export const useLinkProps = ( const onClick = useMemo(() => { return (e: React.MouseEvent | React.MouseEvent) => { + if (e.defaultPrevented || isModifiedEvent(e)) { + return; + } e.preventDefault(); const navigate = () => { @@ -112,3 +115,6 @@ const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { ); } }; + +const isModifiedEvent = (event: any) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index fe11c4cb08d1..8ea236b2dd6c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -32,11 +32,11 @@ export const ManualInstructions: React.FunctionComponent = ({ const macOsLinuxTarCommand = `./elastic-agent enroll ${enrollArgs} ./elastic-agent run`; - const linuxDebRpmCommand = `./elastic-agent enroll ${enrollArgs} + const linuxDebRpmCommand = `elastic-agent enroll ${enrollArgs} systemctl enable elastic-agent systemctl start elastic-agent`; - const windowsCommand = `./elastic-agent enroll ${enrollArgs} + const windowsCommand = `.\elastic-agent enroll ${enrollArgs} ./install-service-elastic-agent.ps1`; return ( @@ -44,7 +44,7 @@ systemctl start elastic-agent`; 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 0e65cb80f07c..b87bd66cce0e 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 @@ -10,7 +10,6 @@ import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, - EuiCallOut, EuiText, EuiSpacer, EuiButtonEmpty, @@ -24,7 +23,7 @@ import styled from 'styled-components'; import { AgentConfig, AgentConfigDetailsDeployAgentAction } from '../../../types'; import { PAGE_ROUTING_PATHS } from '../../../constants'; import { useGetOneAgentConfig, useLink, useBreadcrumbs, useCore } from '../../../hooks'; -import { Loading } from '../../../components'; +import { Loading, Error } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { LinkedAgentCount, AgentConfigActionMenu } from '../components'; @@ -109,97 +108,98 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { }, [routeState, navigateToApp]); const headerRightContent = useMemo( - () => ( - - {[ - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.revision', { - defaultMessage: 'Revision', - }), - content: agentConfig?.revision ?? 0, - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.package_configs', { - defaultMessage: 'Integrations', - }), - content: ( - - ), - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.usedBy', { - defaultMessage: 'Used by', - }), - content: ( - - ), - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.lastUpdated', { - defaultMessage: 'Last updated on', - }), - content: - (agentConfig && ( - + agentConfig ? ( + + {[ + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.revision', { + defaultMessage: 'Revision', + }), + content: agentConfig?.revision ?? 0, + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.package_configs', { + defaultMessage: 'Integrations', + }), + content: ( + - )) || - '', - }, - { isDivider: true }, - { - content: agentConfig && ( - { - history.push(getPath('configuration_details', { configId: newAgentConfig.id })); - }} - enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault} - onCancelEnrollment={ - routeState && routeState.onDoneNavigateTo - ? enrollmentCancelClickHandler - : undefined - } - /> - ), - }, - ].map((item, index) => ( - - {item.isDivider ?? false ? ( - - ) : item.label ? ( - - - {item.label} - - - {item.content} - - - ) : ( - item.content - )} - - ))} - - ), + ), + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.usedBy', { + defaultMessage: 'Used by', + }), + content: ( + + ), + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.lastUpdated', { + defaultMessage: 'Last updated on', + }), + content: + (agentConfig && ( + + )) || + '', + }, + { isDivider: true }, + { + content: agentConfig && ( + { + history.push(getPath('configuration_details', { configId: newAgentConfig.id })); + }} + enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault} + onCancelEnrollment={ + routeState && routeState.onDoneNavigateTo + ? enrollmentCancelClickHandler + : undefined + } + /> + ), + }, + ].map((item, index) => ( + + {item.isDivider ?? false ? ( + + ) : item.label ? ( + + + {item.label} + + + {item.content} + + + ) : ( + item.content + )} + + ))} + + ) : undefined, /* eslint-disable-next-line react-hooks/exhaustive-deps */ [agentConfig, configId, agentStatus] ); @@ -225,45 +225,50 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { ]; }, [getHref, configId, tabId]); - if (redirectToAgentConfigList) { - return ; - } + const content = useMemo(() => { + if (redirectToAgentConfigList) { + return ; + } - if (isLoading) { - return ; - } + if (isLoading) { + return ; + } - if (error) { - return ( - - -

- {error.message} -

-
-
- ); - } + if (error) { + return ( + + } + error={error} + /> + ); + } - if (!agentConfig) { - return ( - - + } + error={i18n.translate('xpack.ingestManager.configDetails.configNotFoundErrorTitle', { + defaultMessage: "Config '{id}' not found", + values: { + id: configId, + }, + })} /> - - ); - } + ); + } + + return ; + }, [agentConfig, configId, error, isLoading, redirectToAgentConfigList]); return ( @@ -273,7 +278,7 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - + {content} diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts index ee03b3faf79d..401211409ebf 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors.ts @@ -15,9 +15,17 @@ export class IngestManagerError extends Error { export const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway + } + if (error instanceof PackageNotFoundError) { + return 404; + } + if (error instanceof PackageOutdatedError) { + return 400; } else { return 400; // Bad Request } }; export class RegistryError extends IngestManagerError {} +export class PackageNotFoundError extends IngestManagerError {} +export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index df37aeb27c75..43ae2b72f607 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -31,12 +31,12 @@ export const getListHandler: RequestHandler = async (context, request, response) must: [ { exists: { - field: 'dataset.namespace', + field: 'data_stream.namespace', }, }, { exists: { - field: 'dataset.name', + field: 'data_stream.dataset', }, }, ], @@ -54,19 +54,19 @@ export const getListHandler: RequestHandler = async (context, request, response) aggs: { dataset: { terms: { - field: 'dataset.name', + field: 'data_stream.dataset', size: 1, }, }, namespace: { terms: { - field: 'dataset.namespace', + field: 'data_stream.namespace', size: 1, }, }, type: { terms: { - field: 'dataset.type', + field: 'data_stream.type', size: 1, }, }, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index f54e61280b98..f8f39f629426 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -32,6 +32,7 @@ import { getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; +import { IngestManagerError, getHTTPResponseCode } from '../../errors'; export const getCategoriesHandler: RequestHandler< undefined, @@ -145,9 +146,11 @@ export const getInfoHandler: RequestHandler> = async (context, request, response) => { +export const installPackageHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { const logger = appContextService.getLogger(); const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; @@ -158,6 +161,7 @@ export const installPackageHandler: RequestHandler isPipeline(path)); - if (datasets) { - const pipelines = datasets.reduce>>((acc, dataset) => { - if (dataset.ingest_pipeline) { - acc.push( - installPipelinesForDataset({ - dataset, - callCluster, - paths: pipelinePaths, - pkgVersion: registryPackage.version, - }) - ); - } - return acc; - }, []); - const pipelinesToSave = await Promise.all(pipelines).then((results) => results.flat()); - return saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelinesToSave); - } - return []; + // get and save pipeline refs before installing pipelines + const pipelineRefs = datasets.reduce((acc, dataset) => { + const filteredPaths = pipelinePaths.filter((path) => isDatasetPipeline(path, dataset.path)); + const pipelineObjectRefs = filteredPaths.map((path) => { + const { name } = getNameAndExtension(path); + const nameForInstallation = getPipelineNameForInstallation({ + pipelineName: name, + dataset, + packageVersion: registryPackage.version, + }); + return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; + }); + acc.push(...pipelineObjectRefs); + return acc; + }, []); + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelineRefs); + const pipelines = datasets.reduce>>((acc, dataset) => { + if (dataset.ingest_pipeline) { + acc.push( + installPipelinesForDataset({ + dataset, + callCluster, + paths: pipelinePaths, + pkgVersion: registryPackage.version, + }) + ); + } + return acc; + }, []); + return await Promise.all(pipelines).then((results) => results.flat()); }; export function rewriteIngestPipeline( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 436a6a1bdc55..2a3120f06490 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -41,6 +41,16 @@ export const installTemplates = async ( ); // build templates per dataset from yml files const datasets = registryPackage.datasets; + if (!datasets) return []; + // get template refs to save + const installedTemplateRefs = datasets.map((dataset) => ({ + id: generateTemplateName(dataset), + type: ElasticsearchAssetType.indexTemplate, + })); + + // add package installation's references to index templates + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); + if (datasets) { const installTemplatePromises = datasets.reduce>>((acc, dataset) => { acc.push( @@ -55,14 +65,6 @@ export const installTemplates = async ( const res = await Promise.all(installTemplatePromises); const installedTemplates = res.flat(); - // get template refs to save - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); - - // add package installation's references to index templates - await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); return installedTemplates; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index a739806d5868..71e49acf1766 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -393,14 +393,14 @@ const updateExistingIndex = async ({ // are added in https://github.com/elastic/kibana/issues/66551. namespace value we will continue // to skip updating and assume the value in the index mapping is correct delete mappings.properties.stream; - delete mappings.properties.dataset; + delete mappings.properties.data_stream; - // get the dataset values from the index template to compose data stream name + // get the data_stream values from the index template to compose data stream name const indexMappings = await getIndexMappings(indexName, callCluster); - const dataset = indexMappings[indexName].mappings.properties.dataset.properties; - if (!dataset.type.value || !dataset.name.value || !dataset.namespace.value) - throw new Error(`dataset values are missing from the index template ${indexName}`); - const dataStreamName = `${dataset.type.value}-${dataset.name.value}-${dataset.namespace.value}`; + const dataStream = indexMappings[indexName].mappings.properties.data_stream.properties; + if (!dataStream.type.value || !dataStream.dataset.value || !dataStream.namespace.value) + throw new Error(`data_stream values are missing from the index template ${indexName}`); + const dataStreamName = `${dataStream.type.value}-${dataStream.dataset.value}-${dataStream.namespace.value}`; // try to update the mappings first try { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index a3fe444b19b1..5741764164b8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,14 +11,8 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { - AssetType, - KibanaAssetType, - AssetReference, - KibanaAssetReference, -} from '../../../../types'; -import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; -import { getInstallationObject, savedObjectTypes } from '../../packages'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { savedObjectTypes } from '../../packages'; type SavedObjectToBe = Required & { type: AssetType }; export type ArchiveAsset = Pick< @@ -28,7 +22,7 @@ export type ArchiveAsset = Pick< type: AssetType; }; -export async function getKibanaAsset(key: string) { +export async function getKibanaAsset(key: string): Promise { const buffer = Registry.getAsset(key); // cache values are buffers. convert to string / JSON @@ -51,31 +45,18 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; - paths: string[]; + kibanaAssets: ArchiveAsset[]; isUpdate: boolean; -}): Promise { - const { savedObjectsClient, paths, pkgName, isUpdate } = options; - - if (isUpdate) { - // delete currently installed kibana saved objects and installation references - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedKibanaRefs = installedPkg?.attributes.installed_kibana; - - if (installedKibanaRefs?.length) { - await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedKibanaRefs); - await deleteKibanaInstalledRefs(savedObjectsClient, pkgName, installedKibanaRefs); - } - } +}): Promise { + const { savedObjectsClient, kibanaAssets } = options; - // install the new assets and save installation references + // install the assets const kibanaAssetTypes = Object.values(KibanaAssetType); const installedAssets = await Promise.all( kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets }) ) ); - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array return installedAssets.flat(); } export const deleteKibanaInstalledRefs = async ( @@ -92,21 +73,25 @@ export const deleteKibanaInstalledRefs = async ( installed_kibana: installedAssetsToSave, }); }; - +export async function getKibanaAssets(paths: string[]) { + const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType; + const filteredPaths = paths.filter(isKibanaAssetType); + const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path))); + return kibanaAssets; +} async function installKibanaSavedObjects({ savedObjectsClient, assetType, - paths, + kibanaAssets, }: { savedObjectsClient: SavedObjectsClientContract; assetType: KibanaAssetType; - paths: string[]; + kibanaAssets: ArchiveAsset[]; }) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const isSameType = (asset: ArchiveAsset) => assetType === asset.type; + const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset)); const toBeSavedObjects = await Promise.all( - kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); if (toBeSavedObjects.length === 0) { @@ -115,13 +100,11 @@ async function installKibanaSavedObjects({ const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { overwrite: true, }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; + return createResults.saved_objects; } } -function toAssetReference({ id, type }: SavedObject) { +export function toAssetReference({ id, type }: SavedObject) { const reference: AssetReference = { id, type: type as KibanaAssetType }; return reference; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index a69daae6e041..1346b75c81e4 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -5,7 +5,6 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import Boom from 'boom'; import semver from 'semver'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { @@ -25,8 +24,15 @@ import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { + installKibanaAssets, + getKibanaAssets, + toAssetReference, + ArchiveAsset, +} from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { deleteKibanaSavedObjectsAssets } from './remove'; +import { PackageOutdatedError } from '../../../errors'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -85,19 +91,24 @@ export async function ensureInstalledPackage(options: { return installation; } -export async function installPackage(options: { +export async function installPackage({ + savedObjectsClient, + pkgkey, + callCluster, + force = false, +}: { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; callCluster: CallESAsCurrentUser; + force?: boolean; }): Promise { - const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - if (semver.lt(pkgVersion, latestPackage.version)) - throw Boom.badRequest('Cannot install or update to an out-of-date package'); + if (semver.lt(pkgVersion, latestPackage.version) && !force) + throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); @@ -124,12 +135,23 @@ export async function installPackage(options: { toSaveESIndexPatterns, }); } - const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) + await deleteKibanaSavedObjectsAssets( + savedObjectsClient, + installedPkg.attributes.installed_kibana + ); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); const installKibanaAssetsPromise = installKibanaAssets({ savedObjectsClient, pkgName, - paths, + kibanaAssets, isUpdate, }); @@ -169,21 +191,14 @@ export async function installPackage(options: { ); } - // get template refs to save const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, type: ElasticsearchAssetType.indexTemplate, })); - - const [installedKibanaAssets] = await Promise.all([ - installKibanaAssetsPromise, - installIndexPatternPromise, - ]); - - await saveInstalledKibanaRefs(savedObjectsClient, pkgName, installedKibanaAssets); + await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); // update to newly installed version when all assets are successfully installed if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs]; + return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs]; } const updateVersion = async ( savedObjectsClient: SavedObjectsClientContract, @@ -230,15 +245,16 @@ export async function createInstallation(options: { return [...installedKibana, ...installedEs]; } -export const saveInstalledKibanaRefs = async ( +export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - installedAssets: KibanaAssetReference[] + kibanaAssets: ArchiveAsset[] ) => { + const assetRefs = kibanaAssets.map(toAssetReference); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_kibana: installedAssets, + installed_kibana: assetRefs, }); - return installedAssets; + return assetRefs; }; export const saveInstalledEsRefs = async ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 81bc5847e6c0..1acf2131dcb0 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -102,10 +102,12 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, - installedObjects: AssetReference[] + installedRefs: AssetReference[] ) { + if (!installedRefs.length) return; + const logger = appContextService.getLogger(); - const deletePromises = installedObjects.map(({ id, type }) => { + const deletePromises = installedRefs.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index c7f2df38fe41..c701762e50b5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -22,6 +22,7 @@ import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; +import { PackageNotFoundError } from '../../../errors'; export { ArchiveEntry } from './extract'; @@ -76,7 +77,7 @@ export async function fetchFindLatestPackage(packageName: string): Promise { + it('returned promise should reject if errors thrown', async () => { + const { savedObjectsClient, callClusterMock } = makeErrorMocks(); + const setupPromise = setupIngestManager(savedObjectsClient, callClusterMock); + await expect(setupPromise).rejects.toThrow('mocked'); + }); +}); + +function makeErrorMocks() { + jest.mock('./app_context'); // else fails w/"Logger not set." + jest.mock('./epm/registry/registry_url', () => { + return { + fetchUrl: () => { + throw new Error('mocked registry#fetchUrl'); + }, + }; + }); + + const callClusterMock = jest.fn(); + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find = jest.fn().mockImplementation(() => { + throw new Error('mocked SO#find'); + }); + savedObjectsClient.get = jest.fn().mockImplementation(() => { + throw new Error('mocked SO#get'); + }); + savedObjectsClient.update = jest.fn().mockImplementation(() => { + throw new Error('mocked SO#update'); + }); + + return { + savedObjectsClient, + callClusterMock, + }; +} diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index c91cae98e17d..4ef093d38879 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -127,6 +127,11 @@ export async function setupIngestManager( // if anything errors, reject/fail onSetupReject(error); } + + // be sure to return the promise because it has the resolved/rejected status attached to it + // otherwise, we effectively return success every time even if there are errors + // because `return undefined` -> `Promise.resolve(undefined)` in an `async` function + return setupIngestStatus; } export async function setupFleet( diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index 08f47a8f1caa..191014606f22 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -36,6 +36,11 @@ export const InstallPackageRequestSchema = { params: schema.object({ pkgkey: schema.string(), }), + body: schema.nullable( + schema.object({ + force: schema.boolean(), + }) + ), }; export const DeletePackageRequestSchema = { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts index 2b007a25667a..21a2ee30a84e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PipelineFormProvider as PipelineForm } from './pipeline_form_provider'; +export { PipelineForm } from './pipeline_form'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 341e15132d35..5279bd718c16 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -16,7 +16,6 @@ import './pipeline_form.scss'; import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; -import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; import { pipelineFormSchema } from './schema'; @@ -48,8 +47,6 @@ export const PipelineForm: React.FunctionComponent = ({ }) => { const [isRequestVisible, setIsRequestVisible] = useState(false); - const [isTestingPipeline, setIsTestingPipeline] = useState(false); - const { processors: initialProcessors, on_failure: initialOnFailureProcessors, @@ -79,10 +76,6 @@ export const PipelineForm: React.FunctionComponent = ({ } }; - const handleTestPipelineClick = () => { - setIsTestingPipeline(true); - }; - const { form } = useForm({ schema: pipelineFormSchema, defaultValue: defaultFormValues, @@ -90,7 +83,6 @@ export const PipelineForm: React.FunctionComponent = ({ }); const onEditorFlyoutOpen = useCallback(() => { - setIsTestingPipeline(false); setIsRequestVisible(false); }, [setIsRequestVisible]); @@ -137,8 +129,6 @@ export const PipelineForm: React.FunctionComponent = ({ onFailure={processorsState.onFailure} onProcessorsUpdate={onProcessorsChangeHandler} hasVersion={Boolean(defaultValue.version)} - isTestButtonDisabled={isTestingPipeline || form.isValid === false} - onTestPipelineClick={handleTestPipelineClick} isEditing={isEditing} /> @@ -198,18 +188,6 @@ export const PipelineForm: React.FunctionComponent = ({ closeFlyout={() => setIsRequestVisible((prevIsRequestVisible) => !prevIsRequestVisible)} /> ) : null} - - {/* Test pipeline flyout */} - {isTestingPipeline ? ( - - processorStateRef.current?.getData() || { processors: [], on_failure: [] } - } - closeFlyout={() => { - setIsTestingPipeline((prevIsTestingPipeline) => !prevIsTestingPipeline); - }} - /> - ) : null} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 0e7a45e8d07b..32beb61039a9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -13,7 +13,7 @@ import { Processor } from '../../../../common/types'; import { getUseField, getFormRow, Field, useKibana } from '../../../shared_imports'; import { - PipelineProcessorsContextProvider, + ProcessorsEditorContextProvider, GlobalOnFailureProcessorsEditor, ProcessorsEditor, OnUpdateHandler, @@ -29,8 +29,6 @@ interface Props { onLoadJson: OnDoneLoadJsonHandler; onProcessorsUpdate: OnUpdateHandler; hasVersion: boolean; - isTestButtonDisabled: boolean; - onTestPipelineClick: () => void; onEditorFlyoutOpen: () => void; isEditing?: boolean; } @@ -45,8 +43,6 @@ export const PipelineFormFields: React.FunctionComponent = ({ onProcessorsUpdate, isEditing, hasVersion, - isTestButtonDisabled, - onTestPipelineClick, onEditorFlyoutOpen, }) => { const { services } = useKibana(); @@ -125,20 +121,18 @@ export const PipelineFormFields: React.FunctionComponent = ({ {/* Pipeline Processors Editor */} -
- + @@ -154,7 +148,7 @@ export const PipelineFormFields: React.FunctionComponent = ({
-
+ ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx deleted file mode 100644 index e6482a9fc12c..000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { PipelineForm as PipelineFormUI, PipelineFormProps } from './pipeline_form'; -import { TestConfigContextProvider } from './test_config_context'; - -export const PipelineFormProvider: React.FunctionComponent = ( - passThroughProps -) => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx deleted file mode 100644 index da5e6cf77364..000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, useEffect, useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiSpacer, - EuiTitle, - EuiCallOut, -} from '@elastic/eui'; - -import { useKibana } from '../../../../shared_imports'; -import { Pipeline } from '../../../../../common/types'; -import { Tabs, Tab, OutputTab, DocumentsTab } from './tabs'; -import { useTestConfigContext } from '../test_config_context'; - -export interface PipelineTestFlyoutProps { - closeFlyout: () => void; - pipeline: Pipeline; - isPipelineValid: boolean; -} - -export const PipelineTestFlyout: React.FunctionComponent = ({ - closeFlyout, - pipeline, - isPipelineValid, -}) => { - const { services } = useKibana(); - - const { testConfig } = useTestConfigContext(); - const { documents: cachedDocuments, verbose: cachedVerbose } = testConfig; - - const initialSelectedTab = cachedDocuments ? 'output' : 'documents'; - const [selectedTab, setSelectedTab] = useState(initialSelectedTab); - - const [shouldExecuteImmediately, setShouldExecuteImmediately] = useState(false); - const [isExecuting, setIsExecuting] = useState(false); - const [executeError, setExecuteError] = useState(null); - const [executeOutput, setExecuteOutput] = useState(undefined); - - const handleExecute = useCallback( - async (documents: object[], verbose?: boolean) => { - const { name: pipelineName, ...pipelineDefinition } = pipeline; - - setIsExecuting(true); - setExecuteError(null); - - const { error, data: output } = await services.api.simulatePipeline({ - documents, - verbose, - pipeline: pipelineDefinition, - }); - - setIsExecuting(false); - - if (error) { - setExecuteError(error); - return; - } - - setExecuteOutput(output); - - services.notifications.toasts.addSuccess( - i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { - defaultMessage: 'Pipeline executed', - }), - { - toastLifeTimeMs: 1000, - } - ); - - setSelectedTab('output'); - }, - [pipeline, services.api, services.notifications.toasts] - ); - - useEffect(() => { - if (cachedDocuments) { - setShouldExecuteImmediately(true); - } - // We only want to know on initial mount if there are cached documents - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - // If the user has already tested the pipeline once, - // use the cached test config and automatically execute the pipeline - if (shouldExecuteImmediately && Object.entries(pipeline).length > 0) { - setShouldExecuteImmediately(false); - handleExecute(cachedDocuments!, cachedVerbose); - } - }, [ - pipeline, - handleExecute, - cachedDocuments, - cachedVerbose, - isExecuting, - shouldExecuteImmediately, - ]); - - let tabContent; - - if (selectedTab === 'output') { - tabContent = ( - - ); - } else { - // default to "documents" tab - tabContent = ( - - ); - } - - return ( - - - -

- {pipeline.name ? ( - - ) : ( - - )} -

-
-
- - - !executeOutput && tabId === 'output'} - /> - - - - {/* Execute error */} - {executeError ? ( - <> - - } - color="danger" - iconType="alert" - > -

{executeError.message}

-
- - - ) : null} - - {/* Invalid pipeline error */} - {!isPipelineValid ? ( - <> - - } - color="danger" - iconType="alert" - /> - - - ) : null} - - {/* Documents or output tab content */} - {tabContent} -
-
- ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx deleted file mode 100644 index 7f91672d64df..000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, useEffect } from 'react'; - -import { Pipeline } from '../../../../../common/types'; -import { useFormContext } from '../../../../shared_imports'; - -import { ReadProcessorsFunction } from '../types'; - -import { PipelineTestFlyout, PipelineTestFlyoutProps } from './pipeline_test_flyout'; - -interface Props extends Omit { - readProcessors: ReadProcessorsFunction; -} - -export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ - closeFlyout, - readProcessors, -}) => { - const form = useFormContext(); - const [formData, setFormData] = useState({} as Pipeline); - const [isFormDataValid, setIsFormDataValid] = useState(false); - - useEffect(() => { - const subscription = form.subscribe(async ({ isValid, validate, data }) => { - const isFormValid = isValid ?? (await validate()); - if (isFormValid) { - setFormData(data.format() as Pipeline); - } - setIsFormDataValid(isFormValid); - }); - - return subscription.unsubscribe; - }, [form]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx index 5e5cddbd36b9..3e8cd999a484 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx @@ -5,25 +5,23 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context'; -import { LoadFromJsonButton, OnDoneLoadJsonHandler } from '../pipeline_processors_editor'; +import { + LoadFromJsonButton, + OnDoneLoadJsonHandler, + TestPipelineButton, +} from '../pipeline_processors_editor'; export interface Props { - onTestPipelineClick: () => void; - isTestButtonDisabled: boolean; onLoadJson: OnDoneLoadJsonHandler; } -export const ProcessorsHeader: FunctionComponent = ({ - onTestPipelineClick, - isTestButtonDisabled, - onLoadJson, -}) => { +export const ProcessorsHeader: FunctionComponent = ({ onLoadJson }) => { const { links } = usePipelineProcessorsContext(); return ( = ({ - - - + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index e7258a74f473..227513dcdaac 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; import { registerTestBed, TestBed } from '../../../../../../../test_utils'; import { - PipelineProcessorsContextProvider, + ProcessorsEditorContextProvider, Props, ProcessorsEditor, GlobalOnFailureProcessorsEditor, @@ -62,9 +62,9 @@ jest.mock('react-virtualized', () => { const testBedSetup = registerTestBed( (props: Props) => ( - + - + ), { doMountAsync: false, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx index df4832f9a45e..a45a677846b2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -3,8 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { notificationServiceMock } from 'src/core/public/mocks'; + import { setup, SetupResult } from './pipeline_processors_editor.helpers'; import { Pipeline } from '../../../../../common/types'; +import { apiService } from '../../../services'; const testProcessors: Pick = { processors: [ @@ -46,6 +49,8 @@ describe('Pipeline Editor', () => { links: { esDocsBasePath: 'test', }, + toasts: notificationServiceMock.createSetupContract().toasts, + api: apiService, }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts index b532b2d953e6..bf724be950fd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -20,4 +20,6 @@ export { ProcessorRemoveModal } from './processor_remove_modal'; export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; +export { TestPipelineButton } from './test_pipeline'; + export { PipelineProcessorsItemTooltip, Position } from './pipeline_processors_editor_item_tooltip'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx index c89ff1d3d99a..ef8bf790a18a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx @@ -21,6 +21,7 @@ export const PipelineProcessorsEditor: FunctionComponent = memo( state: { editor, processors }, } = usePipelineProcessorsContext(); const baseSelector = useMemo(() => [stateSlice], [stateSlice]); + return ( { + return ( + + {(openFlyout) => { + return ( + + {i18nTexts.buttonLabel} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_provider.tsx new file mode 100644 index 000000000000..ad88259e3bcc --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_provider.tsx @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, useEffect, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiCallOut, +} from '@elastic/eui'; + +import { usePipelineProcessorsContext, useTestConfigContext } from '../../context'; +import { serialize } from '../../serialize'; + +import { Tabs, Tab, OutputTab, DocumentsTab } from './flyout_tabs'; + +export interface Props { + children: (openFlyout: () => void) => React.ReactNode; +} + +export const FlyoutProvider: React.FunctionComponent = ({ children }) => { + const { + state: { processors }, + api, + toasts, + } = usePipelineProcessorsContext(); + + const serializedProcessors = serialize(processors.state); + + const { testConfig } = useTestConfigContext(); + const { documents: cachedDocuments, verbose: cachedVerbose } = testConfig; + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + const initialSelectedTab = cachedDocuments ? 'output' : 'documents'; + const [selectedTab, setSelectedTab] = useState(initialSelectedTab); + + const [shouldExecuteImmediately, setShouldExecuteImmediately] = useState(false); + const [isExecuting, setIsExecuting] = useState(false); + const [executeError, setExecuteError] = useState(null); + const [executeOutput, setExecuteOutput] = useState(undefined); + + const handleExecute = useCallback( + async (documents: object[], verbose?: boolean) => { + setIsExecuting(true); + setExecuteError(null); + + const { error, data: output } = await api.simulatePipeline({ + documents, + verbose, + pipeline: { ...serializedProcessors }, + }); + + setIsExecuting(false); + + if (error) { + setExecuteError(error); + return; + } + + setExecuteOutput(output); + + toasts.addSuccess( + i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { + defaultMessage: 'Pipeline executed', + }), + { + toastLifeTimeMs: 1000, + } + ); + + setSelectedTab('output'); + }, + [serializedProcessors, api, toasts] + ); + + useEffect(() => { + if (isFlyoutVisible === false && cachedDocuments) { + setShouldExecuteImmediately(true); + } + }, [isFlyoutVisible, cachedDocuments]); + + useEffect(() => { + // If the user has already tested the pipeline once, + // use the cached test config and automatically execute the pipeline + if (isFlyoutVisible && shouldExecuteImmediately && cachedDocuments) { + setShouldExecuteImmediately(false); + handleExecute(cachedDocuments!, cachedVerbose); + } + }, [handleExecute, cachedDocuments, cachedVerbose, isFlyoutVisible, shouldExecuteImmediately]); + + let tabContent; + + if (selectedTab === 'output') { + tabContent = ( + + ); + } else { + // default to "Documents" tab + tabContent = ; + } + + return ( + <> + {children(() => setIsFlyoutVisible(true))} + + {isFlyoutVisible && ( + setIsFlyoutVisible(false)} + data-test-subj="testPipelineFlyout" + > + + +

+ +

+
+
+ + + !executeOutput && tabId === 'output'} + /> + + + + {/* Execute error */} + {executeError ? ( + <> + + } + color="danger" + iconType="alert" + > +

{executeError.message}

+
+ + + ) : null} + + {/* Documents or output tab content */} + {tabContent} +
+
+ )} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/pipeline_test_tabs.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/pipeline_test_tabs.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/schema.tsx similarity index 96% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/schema.tsx index de9910344bd4..e8ac223d56ed 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/schema.tsx @@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiCode } from '@elastic/eui'; -import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../shared_imports'; -import { parseJson, stringifyJson } from '../../../../lib'; +import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../../shared_imports'; +import { parseJson, stringifyJson } from '../../../../../lib'; const { emptyField, isJsonField } = fieldValidators; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/tab_documents.tsx similarity index 85% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/tab_documents.tsx index be9ebc57c69e..593347f8b234 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/tab_documents.tsx @@ -17,32 +17,27 @@ import { Form, useForm, FormConfig, - useKibana, -} from '../../../../../shared_imports'; +} from '../../../../../../shared_imports'; + +import { usePipelineProcessorsContext, useTestConfigContext, TestConfig } from '../../../context'; import { documentsSchema } from './schema'; -import { useTestConfigContext, TestConfig } from '../../test_config_context'; const UseField = getUseField({ component: Field }); interface Props { handleExecute: (documents: object[], verbose: boolean) => void; - isPipelineValid: boolean; isExecuting: boolean; } -export const DocumentsTab: React.FunctionComponent = ({ - isPipelineValid, - handleExecute, - isExecuting, -}) => { - const { services } = useKibana(); +export const DocumentsTab: React.FunctionComponent = ({ handleExecute, isExecuting }) => { + const { links } = usePipelineProcessorsContext(); const { setCurrentTestConfig, testConfig } = useTestConfigContext(); const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; const executePipeline: FormConfig['onSubmit'] = async (formData, isValid) => { - if (!isValid || !isPipelineValid) { + if (!isValid) { return; } @@ -76,7 +71,7 @@ export const DocumentsTab: React.FunctionComponent = ({ values={{ learnMoreLink: ( @@ -98,7 +93,7 @@ export const DocumentsTab: React.FunctionComponent = ({
{/* Documents editor */} @@ -125,7 +120,7 @@ export const DocumentsTab: React.FunctionComponent = ({ onClick={form.submit} size="s" isLoading={isExecuting} - disabled={(form.isSubmitted && !form.isValid) || !isPipelineValid} + disabled={form.isSubmitted && !form.isValid} > {isExecuting ? ( = ({ + children, + links, + api, + toasts, + onUpdate, + value, + onFlyoutOpen, +}: Props) => { + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts new file mode 100644 index 000000000000..1664b3410c1c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ProcessorsEditorContextProvider } from './context'; + +export { TestConfigContextProvider, useTestConfigContext, TestConfig } from './test_config_context'; + +export { + PipelineProcessorsContextProvider, + usePipelineProcessorsContext, + Props, +} from './processors_context'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx similarity index 91% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx index 098473b0d257..db4629823ef5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx @@ -15,7 +15,10 @@ import React, { useRef, } from 'react'; -import { Processor } from '../../../../common/types'; +import { NotificationsSetup } from 'src/core/public'; + +import { Processor } from '../../../../../common/types'; +import { ApiService } from '../../../services'; import { EditorMode, @@ -26,29 +29,31 @@ import { ContextValueState, Links, ProcessorInternal, -} from './types'; +} from '../types'; -import { useProcessorsState, isOnFailureSelector } from './processors_reducer'; +import { useProcessorsState, isOnFailureSelector } from '../processors_reducer'; -import { deserialize } from './deserialize'; +import { deserialize } from '../deserialize'; -import { serialize } from './serialize'; +import { serialize } from '../serialize'; -import { OnActionHandler } from './components/processors_tree'; +import { OnActionHandler } from '../components/processors_tree'; import { ProcessorRemoveModal, PipelineProcessorsItemTooltip, ProcessorSettingsForm, OnSubmitHandler, -} from './components'; +} from '../components'; -import { getValue } from './utils'; +import { getValue } from '../utils'; const PipelineProcessorsContext = createContext({} as any); export interface Props { links: Links; + api: ApiService; + toasts: NotificationsSetup['toasts']; value: { processors: Processor[]; onFailure?: Processor[]; @@ -62,6 +67,8 @@ export interface Props { export const PipelineProcessorsContextProvider: FunctionComponent = ({ links, + api, + toasts, value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, onUpdate, onFlyoutOpen, @@ -205,6 +212,8 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ ; + pipeline: Pick; }) { const result = await this.sendRequest({ path: `${API_BASE_PATH}/simulate`, diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index 7f6a87a46fea..daed338eb6ab 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -34,10 +34,6 @@ export class DocumentationService { public getPutPipelineApiUrl() { return `${this.esDocBasePath}/put-pipeline-api.html`; } - - public getSimulatePipelineApiUrl() { - return `${this.esDocBasePath}/simulate-pipeline-api.html`; - } } export const documentationService = new DocumentationService(); 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 b30a58648700..f92343183a70 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -196,7 +196,7 @@ describe('Lens App', () => { core.uiSettings.get.mockImplementation( jest.fn((type) => { - if (type === 'timepicker:timeDefaults') { + if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { return { from: 'now-7d', to: 'now' }; } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { return 'kuery'; diff --git a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts index 430e4983882c..f9ef5b8fde5b 100644 --- a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts +++ b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts @@ -6,7 +6,7 @@ import { resolve } from 'path'; -// @ts-ignore +// @ts-expect-error import madge from 'madge'; import { createFailError, run } from '@kbn/dev-utils'; diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index 15779d22681c..7b184819b839 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -134,6 +134,10 @@ export class ESAggField implements IESAggField { supportsAutoDomain(): boolean { return true; } + + canReadFromGeoJson(): boolean { + return true; + } } export function esAggFieldsFactory( diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts index 410b38e79ffe..2c190d54f026 100644 --- a/x-pack/plugins/maps/public/classes/fields/field.ts +++ b/x-pack/plugins/maps/public/classes/fields/field.ts @@ -26,7 +26,12 @@ export interface IField { // then styling properties that require the domain to be known cannot use this property. supportsAutoDomain(): boolean; + // Determinse wheter Maps-app can automatically deterime the domain of the field-values + // _without_ having to retrieve the data as GeoJson + // e.g. for ES-sources, this would use the extended_stats API supportsFieldMeta(): boolean; + + canReadFromGeoJson(): boolean; } export class AbstractField implements IField { @@ -90,4 +95,8 @@ export class AbstractField implements IField { supportsAutoDomain(): boolean { return true; } + + canReadFromGeoJson(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts index eb2bb94b36a6..7c8d08bacdb5 100644 --- a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts @@ -56,4 +56,8 @@ export class MVTField extends AbstractField implements IField { supportsAutoDomain() { return false; } + + canReadFromGeoJson(): boolean { + return false; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts index f4625e42ab5d..fc931b13619e 100644 --- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts @@ -79,4 +79,8 @@ export class TopTermPercentageField implements IESAggField { canValueBeFormatted(): boolean { return false; } + + canReadFromGeoJson(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx similarity index 69% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx index 7e8e6896ef9c..9d5bf85005ae 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx @@ -4,35 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component, ReactElement } from 'react'; import _ from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { Category } from './category'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; + const EMPTY_VALUE = ''; -export class BreakedLegend extends React.Component { - state = { +interface Break { + color: string; + label: ReactElement | string; + symbolId: string; +} + +interface Props { + style: IDynamicStyleProperty; + breaks: Break[]; + isLinesOnly: boolean; + isPointsOnly: boolean; +} + +interface State { + label: string; +} + +export class BreakedLegend extends Component { + private _isMounted: boolean = false; + + state: State = { label: EMPTY_VALUE, }; componentDidMount() { this._isMounted = true; - this._loadParams(); + this._loadLabel(); } componentDidUpdate() { - this._loadParams(); + this._loadLabel(); } componentWillUnmount() { this._isMounted = false; } - async _loadParams() { - const label = await this.props.style.getField().getLabel(); - const newState = { label }; - if (this._isMounted && !_.isEqual(this.state, newState)) { - this.setState(newState); + async _loadLabel() { + const field = this.props.style.getField(); + if (!field) { + return; + } + const label = await field.getLabel(); + if (this._isMounted && !_.isEqual(this.state.label, label)) { + this.setState({ label }); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx similarity index 82% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx index cfdbd728c221..02ca4645dd8c 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { VECTOR_STYLES } from '../../../../../../common/constants'; +import React, { ReactElement } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { VECTOR_STYLES } from '../../../../../../common/constants'; import { VectorIcon } from './vector_icon'; -export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, symbolId }) { +interface Props { + styleName: VECTOR_STYLES; + label: ReactElement | string; + color: string; + isLinesOnly: boolean; + isPointsOnly: boolean; + symbolId: string; +} + +export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, symbolId }: Props) { function renderIcon() { if (styleName === VECTOR_STYLES.LABEL_COLOR) { return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx similarity index 92% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx index 5efba64360f2..0056a2cba02c 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; -export const CircleIcon = ({ style }) => ( +export const CircleIcon = ({ style }: { style: CSSProperties }) => ( ( +export const LineIcon = ({ style }: { style: CSSProperties }) => ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx similarity index 70% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx index 478d96962e47..a99548b6af7b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Component, Fragment } from 'react'; import _ from 'lodash'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +// @ts-expect-error import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; import { VECTOR_STYLES } from '../../../../../../common/constants'; -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { CircleIcon } from './circle_icon'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; function getLineWidthIcons() { const defaultStyle = { @@ -37,41 +39,50 @@ function getSymbolSizeIcons() { } const EMPTY_VALUE = ''; -export class OrdinalLegend extends React.Component { - constructor() { - super(); - this._isMounted = false; - this.state = { - label: EMPTY_VALUE, - }; - } +interface Props { + style: IDynamicStyleProperty; +} - async _loadParams() { - const label = await this.props.style.getField().getLabel(); - const newState = { label }; - if (this._isMounted && !_.isEqual(this.state, newState)) { - this.setState(newState); - } - } +interface State { + label: string; +} - _formatValue(value) { - if (value === EMPTY_VALUE) { - return value; - } - return this.props.style.formatField(value); +export class OrdinalLegend extends Component { + private _isMounted: boolean = false; + + state: State = { + label: EMPTY_VALUE, + }; + + componentDidMount() { + this._isMounted = true; + this._loadLabel(); } componentDidUpdate() { - this._loadParams(); + this._loadLabel(); } componentWillUnmount() { this._isMounted = false; } - componentDidMount() { - this._isMounted = true; - this._loadParams(); + async _loadLabel() { + const field = this.props.style.getField(); + if (!field) { + return; + } + const label = await field.getLabel(); + if (this._isMounted && !_.isEqual(this.state.label, label)) { + this.setState({ label }); + } + } + + _formatValue(value: string | number) { + if (value === EMPTY_VALUE) { + return value; + } + return this.props.style.formatField(value); } _renderRangeLegendHeader() { @@ -115,21 +126,16 @@ export class OrdinalLegend extends React.Component { const fieldMeta = this.props.style.getRangeFieldMeta(); - let minLabel = EMPTY_VALUE; - let maxLabel = EMPTY_VALUE; + let minLabel: string | number = EMPTY_VALUE; + let maxLabel: string | number = EMPTY_VALUE; if (fieldMeta) { - const range = { min: fieldMeta.min, max: fieldMeta.max }; - const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); + const min = this._formatValue(_.get(fieldMeta, 'min', EMPTY_VALUE)); minLabel = - this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange - ? `< ${min}` - : min; + this.props.style.isFieldMetaEnabled() && fieldMeta.isMinOutsideStdRange ? `< ${min}` : min; - const max = this._formatValue(_.get(range, 'max', EMPTY_VALUE)); + const max = this._formatValue(_.get(fieldMeta, 'max', EMPTY_VALUE)); maxLabel = - this.props.style.isFieldMetaEnabled() && range && range.isMaxOutsideStdRange - ? `> ${max}` - : max; + this.props.style.isFieldMetaEnabled() && fieldMeta.isMaxOutsideStdRange ? `> ${max}` : max; } return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx similarity index 78% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx index 4210b59f0d67..09241d538a0f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; -export const PolygonIcon = ({ style }) => ( +export const PolygonIcon = ({ style }: { style: CSSProperties }) => ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx similarity index 83% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx index ea3886c600be..c5d41ae2b1a9 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx @@ -5,13 +5,24 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - +// @ts-expect-error import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; -export class SymbolIcon extends Component { - state = { - imgDataUrl: undefined, +interface Props { + symbolId: string; + fill?: string; + stroke?: string; +} + +interface State { + imgDataUrl: string | null; +} + +export class SymbolIcon extends Component { + private _isMounted: boolean = false; + + state: State = { + imgDataUrl: null, }; componentDidMount() { @@ -62,9 +73,3 @@ export class SymbolIcon extends Component { ); } } - -SymbolIcon.propTypes = { - symbolId: PropTypes.string.isRequired, - fill: PropTypes.string, - stroke: PropTypes.string, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx similarity index 79% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx index e255dceda856..d68bbdae2c17 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx @@ -5,14 +5,21 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import { CircleIcon } from './circle_icon'; import { LineIcon } from './line_icon'; import { PolygonIcon } from './polygon_icon'; import { SymbolIcon } from './symbol_icon'; -export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId }) { +interface Props { + fillColor?: string; + isPointsOnly: boolean; + isLinesOnly: boolean; + strokeColor?: string; + symbolId?: string; +} + +export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId }: Props) { if (isLinesOnly) { const style = { stroke: strokeColor, @@ -44,11 +51,3 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, /> ); } - -VectorIcon.propTypes = { - fillColor: PropTypes.string, - isPointsOnly: PropTypes.bool.isRequired, - isLinesOnly: PropTypes.bool.isRequired, - strokeColor: PropTypes.string, - symbolId: PropTypes.string, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx similarity index 74% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 88eb4109627e..4d50c632bfd6 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -5,8 +5,16 @@ */ import React from 'react'; +import { IStyleProperty } from '../../properties/style_property'; -export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }) { +interface Props { + isLinesOnly: boolean; + isPointsOnly: boolean; + styles: Array>; + symbolId: string; +} + +export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }: Props) { const legendRows = []; for (let i = 0; i < styles.length; i++) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js index cde4d1f20119..e643abcaf8d5 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js @@ -8,7 +8,7 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { makeMbClampedNumberExpression, dynamicRound } from '../style_util'; import { getOrdinalMbColorRampStops, getColorPalette } from '../../color_palettes'; import React from 'react'; -import { COLOR_MAP_TYPE, MB_LOOKUP_FUNCTION } from '../../../../../common/constants'; +import { COLOR_MAP_TYPE } from '../../../../../common/constants'; import { isCategoricalStopsInvalid, getOtherCategoryLabel, @@ -91,10 +91,6 @@ export class DynamicColorProperty extends DynamicStyleProperty { return colors ? colors.length : 0; } - supportsMbFeatureState() { - return true; - } - _getMbColor() { if (!this._field || !this._field.getName()) { return null; @@ -120,7 +116,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { const lessThanFirstStopValue = firstStopValue - 1; return [ 'step', - ['coalesce', ['feature-state', targetName], lessThanFirstStopValue], + ['coalesce', [this.getMbLookupFunction(), targetName], lessThanFirstStopValue], RGBA_0000, // MB will assign the base value to any features that is below the first stop value ...colorStops, ]; @@ -146,7 +142,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { makeMbClampedNumberExpression({ minValue: rangeFieldMeta.min, maxValue: rangeFieldMeta.max, - lookupFunction: MB_LOOKUP_FUNCTION.FEATURE_STATE, + lookupFunction: this.getMbLookupFunction(), fallback: lessThanFirstStopValue, fieldName: targetName, }), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js index 2183a298a284..425954c9af86 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js @@ -343,6 +343,15 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { }); describe('custom color ramp', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + useCustomColorRamp: true, + customColorRamp: [ + { stop: 10, color: '#f7faff' }, + { stop: 100, color: '#072f6b' }, + ], + }; + test('should return null when customColorRamp is not provided', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, @@ -362,15 +371,7 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { expect(colorProperty._getMbColor()).toBeNull(); }); - test('should return mapbox expression for custom color ramp', async () => { - const dynamicStyleOptions = { - type: COLOR_MAP_TYPE.ORDINAL, - useCustomColorRamp: true, - customColorRamp: [ - { stop: 10, color: '#f7faff' }, - { stop: 100, color: '#072f6b' }, - ], - }; + test('should use `feature-state` by default', async () => { const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toEqual([ 'step', @@ -382,6 +383,23 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { '#072f6b', ]); }); + + test('should use `get` when source cannot return raw geojson', async () => { + const field = Object.create(mockField); + field.canReadFromGeoJson = function () { + return false; + }; + const colorProperty = makeProperty(dynamicStyleOptions, undefined, field); + expect(colorProperty._getMbColor()).toEqual([ + 'step', + ['coalesce', ['get', 'foobar'], 9], + 'rgba(0,0,0,0)', + 10, + '#f7faff', + 100, + '#072f6b', + ]); + }); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index 132c0b3f2760..81513818bc0e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -21,15 +21,21 @@ import { DynamicIconProperty } from './dynamic_icon_property'; import { mockField, MockLayer } from './__tests__/test_util'; import { IconDynamicOptions } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; +import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; const makeProperty = (options: Partial, field: IField = mockField) => { + const defaultOptions: IconDynamicOptions = { + iconPaletteId: null, + fieldMetaOptions: { isEnabled: false }, + }; + const mockVectorLayer = (new MockLayer() as unknown) as IVectorLayer; return new DynamicIconProperty( - { ...options, fieldMetaOptions: { isEnabled: false } }, + { ...defaultOptions, ...options }, VECTOR_STYLES.ICON, field, - new MockLayer(), + mockVectorLayer, () => { - return (x: string) => x + '_format'; + return (value: string | number | undefined) => value + '_format'; } ); }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx similarity index 79% rename from x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx index 665317569e5e..0d152534aba6 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx @@ -6,13 +6,17 @@ import _ from 'lodash'; import React from 'react'; +import { EuiTextColor } from '@elastic/eui'; +import { Map as MbMap } from 'mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; +// @ts-expect-error import { getIconPalette, getMakiIconId, getMakiSymbolAnchor } from '../symbol_utils'; import { BreakedLegend } from '../components/legend/breaked_legend'; import { getOtherCategoryLabel, assignCategoriesToPalette } from '../style_util'; -import { EuiTextColor } from '@elastic/eui'; +import { LegendProps } from './style_property'; +import { IconDynamicOptions } from '../../../../../common/descriptor_types'; -export class DynamicIconProperty extends DynamicStyleProperty { +export class DynamicIconProperty extends DynamicStyleProperty { isOrdinal() { return false; } @@ -26,7 +30,7 @@ export class DynamicIconProperty extends DynamicStyleProperty { return palette.length; } - syncIconWithMb(symbolLayerId, mbMap, iconPixelSize) { + syncIconWithMb(symbolLayerId: string, mbMap: MbMap, iconPixelSize: number) { if (this._isIconDynamicConfigComplete()) { mbMap.setLayoutProperty( symbolLayerId, @@ -64,11 +68,11 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); } - _getMbIconImageExpression(iconPixelSize) { + _getMbIconImageExpression(iconPixelSize: number) { const { stops, fallbackSymbolId } = this._getPaletteStops(); if (stops.length < 1 || !fallbackSymbolId) { - //occurs when no data + // occurs when no data return null; } @@ -79,16 +83,16 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); if (fallbackSymbolId) { - mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); //last item is fallback style for anything that does not match provided stops + mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); // last item is fallback style for anything that does not match provided stops } - return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; + return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops]; } _getMbIconAnchorExpression() { const { stops, fallbackSymbolId } = this._getPaletteStops(); if (stops.length < 1 || !fallbackSymbolId) { - //occurs when no data + // occurs when no data return null; } @@ -99,16 +103,16 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); if (fallbackSymbolId) { - mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); //last item is fallback style for anything that does not match provided stops + mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); // last item is fallback style for anything that does not match provided stops } - return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; + return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops]; } _isIconDynamicConfigComplete() { return this._field && this._field.isValid(); } - renderLegendDetailRow({ isPointsOnly, isLinesOnly }) { + renderLegendDetailRow({ isPointsOnly, isLinesOnly }: LegendProps) { const { stops, fallbackSymbolId } = this._getPaletteStops(); const breaks = []; stops.forEach(({ stop, style }) => { 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 662d1ccf33b9..83bd4b70ba5c 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 @@ -33,7 +33,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { return false; } - return true; + return super.supportsMbFeatureState(); } syncHaloWidthWithMb(mbLayerId, mbMap) { @@ -109,17 +109,13 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _getMbDataDrivenSize({ targetName, minSize, maxSize, minValue, maxValue }) { - const lookup = this.supportsMbFeatureState() - ? MB_LOOKUP_FUNCTION.FEATURE_STATE - : MB_LOOKUP_FUNCTION.GET; - const stops = minValue === maxValue ? [maxValue, maxSize] : [minValue, minSize, maxValue, maxSize]; return [ 'interpolate', ['linear'], makeMbClampedNumberExpression({ - lookupFunction: lookup, + lookupFunction: this.getMbLookupFunction(), maxValue, minValue, fieldName: targetName, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 216fde595af3..18b7faf6283c 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -11,9 +11,10 @@ import { Feature } from 'geojson'; import { AbstractStyleProperty, IStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { - STYLE_TYPE, - SOURCE_META_DATA_REQUEST_ID, FIELD_ORIGIN, + MB_LOOKUP_FUNCTION, + SOURCE_META_DATA_REQUEST_ID, + STYLE_TYPE, VECTOR_STYLES, } from '../../../../../common/constants'; import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; @@ -21,8 +22,8 @@ import { CategoricalFieldMetaPopover } from '../components/field_meta/categorica import { CategoryFieldMeta, FieldMetaOptions, - StyleMetaData, RangeFieldMeta, + StyleMetaData, } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; @@ -41,12 +42,13 @@ export interface IDynamicStyleProperty extends IStyleProperty { supportsFieldMeta(): boolean; getFieldMetaRequest(): Promise; supportsMbFeatureState(): boolean; + getMbLookupFunction(): MB_LOOKUP_FUNCTION; pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null; pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null; getValueSuggestions(query: string): Promise; } -type fieldFormatter = (value: string | undefined) => string; +type fieldFormatter = (value: string | number | undefined) => string | number; export class DynamicStyleProperty extends AbstractStyleProperty implements IDynamicStyleProperty { @@ -69,12 +71,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty this._getFieldFormatter = getFieldFormatter; } - // ignore TS error about "Type '(query: string) => Promise | never[]' is not assignable to type '(query: string) => Promise'." - // @ts-expect-error - getValueSuggestions = (query: string) => { + getValueSuggestions = async (query: string) => { return this._field === null ? [] - : this._field.getSource().getValueSuggestions(this._field, query); + : await this._field.getSource().getValueSuggestions(this._field, query); }; _getStyleMetaDataRequestId(fieldName: string) { @@ -195,7 +195,13 @@ export class DynamicStyleProperty extends AbstractStyleProperty } supportsMbFeatureState() { - return true; + return !!this._field && this._field.canReadFromGeoJson(); + } + + getMbLookupFunction(): MB_LOOKUP_FUNCTION { + return this.supportsMbFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; } getFieldMetaOptions() { @@ -306,7 +312,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty }; } - formatField(value: string | undefined): string { + formatField(value: string | number | undefined): string | number { if (this.getField()) { const fieldName = this.getFieldName(); const fieldFormatter = this._getFieldFormatter(fieldName); @@ -316,10 +322,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty } } - renderLegendDetailRow() { - return null; - } - renderFieldMetaPopover(onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void) { if (!this.supportsFieldMeta()) { return null; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts index 7a0ed4fb3e96..ec52f6a0f728 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts @@ -11,7 +11,7 @@ import { getVectorStyleLabel } from '../components/get_vector_style_label'; import { FieldMetaOptions } from '../../../../../common/descriptor_types'; import { VECTOR_STYLES } from '../../../../../common/constants'; -type LegendProps = { +export type LegendProps = { isPointsOnly: boolean; isLinesOnly: boolean; symbolId?: string; @@ -20,7 +20,7 @@ type LegendProps = { export interface IStyleProperty { isDynamic(): boolean; isComplete(): boolean; - formatField(value: string | undefined): string; + formatField(value: string | number | undefined): string | number; getStyleName(): VECTOR_STYLES; getOptions(): T; renderLegendDetailRow(legendProps: LegendProps): ReactElement | null; @@ -53,7 +53,7 @@ export class AbstractStyleProperty implements IStyleProperty { return true; } - formatField(value: string | undefined): string { + formatField(value: string | number | undefined): string | number { // eslint-disable-next-line eqeqeq return value == undefined ? '' : value; } @@ -66,7 +66,7 @@ export class AbstractStyleProperty implements IStyleProperty { return this._options; } - renderLegendDetailRow() { + renderLegendDetailRow({ isPointsOnly, isLinesOnly }: LegendProps): ReactElement | null { return null; } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 137b3a31b795..616d06a5c7b1 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -81,6 +81,8 @@ export class MapEmbeddable extends Embeddable { const { refresh } = useRefreshAnalyticsList({ isLoading: setIsLoading }); return ( diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx index 3ad749c9d063..04ce7f79e1c0 100644 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx @@ -25,13 +25,11 @@ import { EuiInMemoryTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useMlKibana } from '../contexts/kibana'; import { SavedObjectDashboard } from '../../../../../../src/plugins/dashboard/public'; -import { - ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - getDefaultPanelTitle, -} from '../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; +import { getDefaultPanelTitle } from '../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; import { useDashboardService } from '../services/dashboard_service'; import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; import { JobId } from '../../../common/types/anomaly_detection_jobs'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../embeddables'; export interface DashboardItem { id: string; diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 51ea0f00d5f6..0fefa71dea48 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -15,12 +15,9 @@ import { } from '@elastic/eui'; import { throttle } from 'lodash'; -import { - ExplorerSwimlane, - ExplorerSwimlaneProps, -} from '../../application/explorer/explorer_swimlane'; +import { ExplorerSwimlane, ExplorerSwimlaneProps } from './explorer_swimlane'; -import { MlTooltipComponent } from '../../application/components/chart_tooltip'; +import { MlTooltipComponent } from '../components/chart_tooltip'; import { SwimLanePagination } from './swimlane_pagination'; import { SWIMLANE_TYPE } from './explorer_constants'; import { ViewBySwimLaneData } from './explorer_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js index ecb862686788..7c55ed01f432 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export const RefreshJobsListButton = ({ onRefreshClick, isRefreshing }) => ( diff --git a/x-pack/plugins/ml/public/application/management/_index.scss b/x-pack/plugins/ml/public/application/management/_index.scss deleted file mode 100644 index e14df2d7c203..000000000000 --- a/x-pack/plugins/ml/public/application/management/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'jobs_list/index'; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/_index.scss b/x-pack/plugins/ml/public/application/management/jobs_list/_index.scss index 841415620d69..d4928a4126c1 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/_index.scss +++ b/x-pack/plugins/ml/public/application/management/jobs_list/_index.scss @@ -1 +1,4 @@ -@import 'components/index'; +// Kibana management page ML section +#kibanaManagementMLSection { + @import 'components/index'; +} 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 b16f680a2a36..81190a412abc 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 @@ -12,6 +12,7 @@ import { MlStartDependencies } from '../../../plugin'; import { JobsListPage } from './components'; import { getJobsListBreadcrumbs } from '../breadcrumbs'; import { setDependencyCache, clearCache } from '../../util/dependency_cache'; +import './_index.scss'; const renderApp = (element: HTMLElement, coreStart: CoreStart) => { ReactDOM.render(React.createElement(JobsListPage, { coreStart }), element); diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index db58b6a537e0..38a7900916ba 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect, FC } from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import { NavigateToPath } from '../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 6486db818e11..1f122ed18a85 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -6,7 +6,7 @@ import { isEqual } from 'lodash'; import React, { FC, useCallback, useEffect, useState } from 'react'; -import { usePrevious } from 'react-use'; +import usePrevious from 'react-use/lib/usePrevious'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/routing/use_refresh.ts b/x-pack/plugins/ml/public/application/routing/use_refresh.ts index 539ce6f88a42..332677e3c579 100644 --- a/x-pack/plugins/ml/public/application/routing/use_refresh.ts +++ b/x-pack/plugins/ml/public/application/routing/use_refresh.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { merge } from 'rxjs'; import { map } from 'rxjs/operators'; diff --git a/x-pack/plugins/ml/public/application/util/string_utils.ts b/x-pack/plugins/ml/public/application/util/string_utils.ts index 55dd16082a07..88900c8b0ce7 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.ts @@ -7,7 +7,6 @@ /* * Contains utility functions for performing operations on Strings. */ -import _ from 'lodash'; import d3 from 'd3'; import he from 'he'; @@ -28,7 +27,8 @@ export function replaceStringTokens( ) { return String(str).replace(/\$([^?&$\'"]+)\$/g, (match, name) => { // Use lodash get to allow nested JSON fields to be retrieved. - let tokenValue = _.get(valuesByTokenName, name, null); + let tokenValue = + valuesByTokenName && valuesByTokenName[name] !== undefined ? valuesByTokenName[name] : null; if (encodeForURI === true && tokenValue !== null) { tokenValue = encodeURIComponent(tokenValue); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 9f96b73d67c5..e837cabf0b49 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -9,29 +9,17 @@ import ReactDOM from 'react-dom'; import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { Subject } from 'rxjs'; -import { - Embeddable, - EmbeddableInput, - EmbeddableOutput, - IContainer, - IEmbeddable, -} from '../../../../../../src/plugins/embeddable/public'; +import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container'; -import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import { JobId } from '../../../common/types/anomaly_detection_jobs'; -import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service'; -import { - Filter, - Query, - RefreshInterval, - TimeRange, -} from '../../../../../../src/plugins/data/common'; -import { SwimlaneType } from '../../application/explorer/explorer_constants'; import { MlDependencies } from '../../application/app'; -import { AppStateSelectedCells } from '../../application/explorer/explorer_utils'; -import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions/triggers'; - -export const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane'; +import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; +import { + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + AnomalySwimlaneEmbeddableInput, + AnomalySwimlaneEmbeddableOutput, + AnomalySwimlaneServices, +} from '..'; export const getDefaultPanelTitle = (jobIds: JobId[]) => i18n.translate('xpack.ml.swimlaneEmbeddable.title', { @@ -39,51 +27,7 @@ export const getDefaultPanelTitle = (jobIds: JobId[]) => values: { jobIds: jobIds.join(', ') }, }); -export interface AnomalySwimlaneEmbeddableCustomInput { - jobIds: JobId[]; - swimlaneType: SwimlaneType; - viewBy?: string; - perPage?: number; - - // Embeddable inputs which are not included in the default interface - filters: Filter[]; - query: Query; - refreshConfig: RefreshInterval; - timeRange: TimeRange; -} - -export interface EditSwimlanePanelContext { - embeddable: IEmbeddable; -} - -export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext { - /** - * Optional data provided by swim lane selection - */ - data?: AppStateSelectedCells; -} - -export type AnomalySwimlaneEmbeddableInput = EmbeddableInput & AnomalySwimlaneEmbeddableCustomInput; - -export type AnomalySwimlaneEmbeddableOutput = EmbeddableOutput & - AnomalySwimlaneEmbeddableCustomOutput; - -export interface AnomalySwimlaneEmbeddableCustomOutput { - perPage?: number; - fromPage?: number; - interval?: number; -} - -export interface AnomalySwimlaneServices { - anomalyDetectorService: AnomalyDetectorService; - anomalyTimelineService: AnomalyTimelineService; -} - -export type AnomalySwimlaneEmbeddableServices = [ - CoreStart, - MlDependencies, - AnomalySwimlaneServices -]; +export type IAnomalySwimlaneEmbeddable = typeof AnomalySwimlaneEmbeddable; export class AnomalySwimlaneEmbeddable extends Embeddable< AnomalySwimlaneEmbeddableInput, diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx index 243369982ac1..12813ad6277a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx @@ -7,10 +7,8 @@ import { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane_embeddable_factory'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; -import { - AnomalySwimlaneEmbeddable, - AnomalySwimlaneEmbeddableInput, -} from './anomaly_swimlane_embeddable'; +import { AnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; +import { AnomalySwimlaneEmbeddableInput } from '..'; jest.mock('./anomaly_swimlane_embeddable', () => ({ AnomalySwimlaneEmbeddable: jest.fn(), diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index 14fbf77544b2..9d2fd07e11be 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -10,23 +10,16 @@ import { StartServicesAccessor } from 'kibana/public'; import { EmbeddableFactoryDefinition, - ErrorEmbeddable, IContainer, } from '../../../../../../src/plugins/embeddable/public'; +import { HttpService } from '../../application/services/http_service'; +import { MlPluginStart, MlStartDependencies } from '../../plugin'; +import { MlDependencies } from '../../application/app'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - AnomalySwimlaneEmbeddable, AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableServices, -} from './anomaly_swimlane_embeddable'; -import { HttpService } from '../../application/services/http_service'; -import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; -import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service'; -import { mlResultsServiceProvider } from '../../application/services/results_service'; -import { resolveAnomalySwimlaneUserInput } from './anomaly_swimlane_setup_flyout'; -import { mlApiServicesProvider } from '../../application/services/ml_api_service'; -import { MlPluginStart, MlStartDependencies } from '../../plugin'; -import { MlDependencies } from '../../application/app'; +} from '..'; export class AnomalySwimlaneEmbeddableFactory implements EmbeddableFactoryDefinition { @@ -50,6 +43,7 @@ export class AnomalySwimlaneEmbeddableFactory const [coreStart] = await this.getServices(); try { + const { resolveAnomalySwimlaneUserInput } = await import('./anomaly_swimlane_setup_flyout'); return await resolveAnomalySwimlaneUserInput(coreStart); } catch (e) { return Promise.reject(); @@ -59,6 +53,15 @@ export class AnomalySwimlaneEmbeddableFactory private async getServices(): Promise { const [coreStart, pluginsStart] = await this.getStartServices(); + const { AnomalyDetectorService } = await import( + '../../application/services/anomaly_detector_service' + ); + const { AnomalyTimelineService } = await import( + '../../application/services/anomaly_timeline_service' + ); + const { mlApiServicesProvider } = await import('../../application/services/ml_api_service'); + const { mlResultsServiceProvider } = await import('../../application/services/results_service'); + const httpService = new HttpService(coreStart.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); const anomalyTimelineService = new AnomalyTimelineService( @@ -77,8 +80,9 @@ export class AnomalySwimlaneEmbeddableFactory public async create( initialInput: AnomalySwimlaneEmbeddableInput, parent?: IContainer - ): Promise { + ): Promise { const services = await this.getServices(); + const { AnomalySwimlaneEmbeddable } = await import('./anomaly_swimlane_embeddable'); return new AnomalySwimlaneEmbeddable(initialInput, services, parent); } } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx index e5a13adca05d..026d4e225f45 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx @@ -22,7 +22,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants'; -import { AnomalySwimlaneEmbeddableInput } from './anomaly_swimlane_embeddable'; +import { AnomalySwimlaneEmbeddableInput } from '..'; export interface AnomalySwimlaneInitializerProps { defaultTitle: string; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index 1ffdadb60aaa..3a3597a7fa92 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -16,12 +16,10 @@ import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer'; import { JobSelectorFlyout } from '../../application/components/job_selector/job_selector_flyout'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import { getInitialGroupsMap } from '../../application/components/job_selector/job_selector'; -import { - AnomalySwimlaneEmbeddableInput, - getDefaultPanelTitle, -} from './anomaly_swimlane_embeddable'; +import { getDefaultPanelTitle } from './anomaly_swimlane_embeddable'; import { getMlGlobalServices } from '../../application/app'; import { HttpService } from '../../application/services/http_service'; +import { AnomalySwimlaneEmbeddableInput } from '..'; export async function resolveAnomalySwimlaneUserInput( coreStart: CoreStart, diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx index 23045834eae5..ff621953cc57 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx @@ -12,11 +12,7 @@ import { } from './embeddable_swim_lane_container'; import { BehaviorSubject, Observable } from 'rxjs'; import { I18nProvider } from '@kbn/i18n/react'; -import { - AnomalySwimlaneEmbeddable, - AnomalySwimlaneEmbeddableInput, - AnomalySwimlaneServices, -} from './anomaly_swimlane_embeddable'; +import { AnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; import { CoreStart } from 'kibana/public'; import { useSwimlaneInputResolver } from './swimlane_input_resolver'; import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; @@ -25,6 +21,7 @@ import { MlDependencies } from '../../application/app'; import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks'; import { TriggerContract } from 'src/plugins/ui_actions/public/triggers'; import { TriggerId } from 'src/plugins/ui_actions/public'; +import { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneServices } from '..'; jest.mock('./swimlane_input_resolver', () => ({ useSwimlaneInputResolver: jest.fn(() => { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 8ee4e391fcdd..60681446ac7a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -10,12 +10,7 @@ import { Observable } from 'rxjs'; import { CoreStart } from 'kibana/public'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - AnomalySwimlaneEmbeddable, - AnomalySwimlaneEmbeddableInput, - AnomalySwimlaneEmbeddableOutput, - AnomalySwimlaneServices, -} from './anomaly_swimlane_embeddable'; +import { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; import { useSwimlaneInputResolver } from './swimlane_input_resolver'; import { SwimlaneType } from '../../application/explorer/explorer_constants'; import { @@ -24,11 +19,16 @@ import { } from '../../application/explorer/swimlane_container'; import { AppStateSelectedCells } from '../../application/explorer/explorer_utils'; import { MlDependencies } from '../../application/app'; -import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions/triggers'; +import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; +import { + AnomalySwimlaneEmbeddableInput, + AnomalySwimlaneEmbeddableOutput, + AnomalySwimlaneServices, +} from '..'; export interface ExplorerSwimlaneContainerProps { id: string; - embeddableContext: AnomalySwimlaneEmbeddable; + embeddableContext: InstanceType; embeddableInput: Observable; services: [CoreStart, MlDependencies, AnomalySwimlaneServices]; refresh: Observable; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts index c0b02960d514..ba2e1c88b3ea 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts @@ -5,4 +5,3 @@ */ export { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane_embeddable_factory'; -export { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from './anomaly_swimlane_embeddable'; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts index a34955adebf6..258b72067cdd 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts @@ -8,12 +8,9 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { processFilters, useSwimlaneInputResolver } from './swimlane_input_resolver'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; -import { - AnomalySwimlaneEmbeddableInput, - AnomalySwimlaneServices, -} from './anomaly_swimlane_embeddable'; import { CoreStart, IUiSettingsClient } from 'kibana/public'; import { MlStartDependencies } from '../../plugin'; +import { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneServices } from '..'; describe('useSwimlaneInputResolver', () => { let embeddableInput: BehaviorSubject>; 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 f17c779a0025..6ddb1e954e57 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 @@ -20,11 +20,6 @@ import { } from 'rxjs/operators'; import { CoreStart } from 'kibana/public'; import { TimeBuckets } from '../../application/util/time_buckets'; -import { - AnomalySwimlaneEmbeddableInput, - AnomalySwimlaneEmbeddableOutput, - AnomalySwimlaneServices, -} from './anomaly_swimlane_embeddable'; import { MlStartDependencies } from '../../plugin'; import { ANOMALY_SWIM_LANE_HARD_LIMIT, @@ -41,6 +36,11 @@ import { AnomalyDetectorService } from '../../application/services/anomaly_detec import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/apply_influencer_filters_action'; +import { + AnomalySwimlaneEmbeddableInput, + AnomalySwimlaneEmbeddableOutput, + AnomalySwimlaneServices, +} from '..'; const FETCH_RESULTS_DEBOUNCE_MS = 500; diff --git a/x-pack/plugins/ml/public/embeddables/constants.ts b/x-pack/plugins/ml/public/embeddables/constants.ts new file mode 100644 index 000000000000..054cb8ba4b0b --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/constants.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 const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane'; diff --git a/x-pack/plugins/ml/public/embeddables/index.ts b/x-pack/plugins/ml/public/embeddables/index.ts index db9f094d5721..cc4bec0b6783 100644 --- a/x-pack/plugins/ml/public/embeddables/index.ts +++ b/x-pack/plugins/ml/public/embeddables/index.ts @@ -8,6 +8,9 @@ import { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane'; import { MlCoreSetup } from '../plugin'; import { EmbeddableSetup } from '../../../../../src/plugins/embeddable/public'; +export * from './constants'; +export * from './types'; + export function registerEmbeddables(embeddable: EmbeddableSetup, core: MlCoreSetup) { const anomalySwimlaneEmbeddableFactory = new AnomalySwimlaneEmbeddableFactory( core.getStartServices diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts new file mode 100644 index 000000000000..93ec79d9b831 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/types.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 { CoreStart } from 'kibana/public'; +import { JobId } from '../../common/types/anomaly_detection_jobs'; +import { SwimlaneType } from '../application/explorer/explorer_constants'; +import { Filter } from '../../../../../src/plugins/data/common/es_query/filters'; +import { Query, RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; +import { + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, +} from '../../../../../src/plugins/embeddable/public'; +import { AnomalyDetectorService } from '../application/services/anomaly_detector_service'; +import { AnomalyTimelineService } from '../application/services/anomaly_timeline_service'; +import { MlDependencies } from '../application/app'; +import { AppStateSelectedCells } from '../application/explorer/explorer_utils'; + +export interface AnomalySwimlaneEmbeddableCustomInput { + jobIds: JobId[]; + swimlaneType: SwimlaneType; + viewBy?: string; + perPage?: number; + + // Embeddable inputs which are not included in the default interface + filters: Filter[]; + query: Query; + refreshConfig: RefreshInterval; + timeRange: TimeRange; +} + +export type AnomalySwimlaneEmbeddableInput = EmbeddableInput & AnomalySwimlaneEmbeddableCustomInput; + +export interface AnomalySwimlaneServices { + anomalyDetectorService: AnomalyDetectorService; + anomalyTimelineService: AnomalyTimelineService; +} + +export type AnomalySwimlaneEmbeddableServices = [ + CoreStart, + MlDependencies, + AnomalySwimlaneServices +]; + +export interface AnomalySwimlaneEmbeddableCustomOutput { + perPage?: number; + fromPage?: number; + interval?: number; +} + +export type AnomalySwimlaneEmbeddableOutput = EmbeddableOutput & + AnomalySwimlaneEmbeddableCustomOutput; + +export interface EditSwimlanePanelContext { + embeddable: IEmbeddable; +} + +export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext { + /** + * Optional data provided by swim lane selection + */ + data?: AppStateSelectedCells; +} diff --git a/x-pack/plugins/ml/public/index.scss b/x-pack/plugins/ml/public/index.scss deleted file mode 100644 index 9bd47b647337..000000000000 --- a/x-pack/plugins/ml/public/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './application/index'; diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 5a956651c86d..80308977735d 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -5,7 +5,6 @@ */ import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; -import './index.scss'; import { MlPlugin, MlPluginSetup, diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index a8e1e804c2fe..aa6163379f9c 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -6,36 +6,35 @@ import { i18n } from '@kbn/i18n'; import { - Plugin, - CoreStart, - CoreSetup, AppMountParameters, + CoreSetup, + CoreStart, + Plugin, PluginInitializerContext, } from 'kibana/public'; import { BehaviorSubject } from 'rxjs'; import { take } from 'rxjs/operators'; import { ManagementSetup } from 'src/plugins/management/public'; -import { SharePluginSetup, SharePluginStart, UrlGeneratorState } from 'src/plugins/share/public'; +import { SharePluginSetup, SharePluginStart } from 'src/plugins/share/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { EmbeddableSetup } from 'src/plugins/embeddable/public'; -import { AppStatus, AppUpdater } from '../../../../src/core/public'; +import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { SecurityPluginSetup } from '../../security/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { registerManagementSection } from './application/management'; import { LicenseManagementUIPluginSetup } from '../../license_management/public'; import { setDependencyCache } from './application/util/dependency_cache'; -import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; +import { PLUGIN_ICON, PLUGIN_ID } from '../common/constants/app'; import { registerFeature } from './register_feature'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -import { registerEmbeddables } from './embeddables'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { registerMlUiActions } from './ui_actions'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { registerUrlGenerator, MlUrlGeneratorState, ML_APP_URL_GENERATOR } from './url_generator'; -import { isMlEnabled, isFullLicense } from '../common/license'; +import { registerUrlGenerator } from './url_generator'; +import { isFullLicense, isMlEnabled } from '../common/license'; +import { registerEmbeddables } from './embeddables'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -56,12 +55,6 @@ export interface MlSetupDependencies { share: SharePluginSetup; } -declare module '../../../../src/plugins/share/public' { - export interface UrlGeneratorStateMapping { - [ML_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - export type MlCoreSetup = CoreSetup; export class MlPlugin implements Plugin { diff --git a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx index 3af39993d39f..9e50410751c3 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx @@ -6,13 +6,10 @@ import { i18n } from '@kbn/i18n'; import { ActionContextMapping, createAction } from '../../../../../src/plugins/ui_actions/public'; -import { - AnomalySwimlaneEmbeddable, - SwimLaneDrilldownContext, -} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; import { MlCoreSetup } from '../plugin'; import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from '../application/explorer/explorer_constants'; import { Filter, FilterStateStore } from '../../../../../src/plugins/data/common'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; export const APPLY_INFLUENCER_FILTERS_ACTION = 'applyInfluencerFiltersAction'; @@ -73,7 +70,7 @@ export function createApplyInfluencerFiltersAction( async isCompatible({ embeddable, data }: SwimLaneDrilldownContext) { // Only compatible with view by influencer swim lanes and single selection return ( - embeddable instanceof AnomalySwimlaneEmbeddable && + embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE && data !== undefined && data.type === SWIMLANE_TYPE.VIEW_BY && data.viewByFieldName !== VIEW_BY_JOB_LABEL && diff --git a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx index ec59ba20acf9..325e903de0e2 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx @@ -7,11 +7,8 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { ActionContextMapping, createAction } from '../../../../../src/plugins/ui_actions/public'; -import { - AnomalySwimlaneEmbeddable, - SwimLaneDrilldownContext, -} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; import { MlCoreSetup } from '../plugin'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; export const APPLY_TIME_RANGE_SELECTION_ACTION = 'applyTimeRangeSelectionAction'; @@ -52,7 +49,7 @@ export function createApplyTimeRangeSelectionAction( }); }, async isCompatible({ embeddable, data }: SwimLaneDrilldownContext) { - return embeddable instanceof AnomalySwimlaneEmbeddable && data !== undefined; + return embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE && data !== undefined; }, }); } diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx index cfd90f92e323..c40d1e175ec7 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx @@ -6,13 +6,9 @@ import { i18n } from '@kbn/i18n'; import { ActionContextMapping, createAction } from '../../../../../src/plugins/ui_actions/public'; -import { - AnomalySwimlaneEmbeddable, - EditSwimlanePanelContext, -} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; -import { resolveAnomalySwimlaneUserInput } from '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout'; import { ViewMode } from '../../../../../src/plugins/embeddable/public'; import { MlCoreSetup } from '../plugin'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, EditSwimlanePanelContext } from '../embeddables'; export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction'; @@ -27,7 +23,7 @@ export function createEditSwimlanePanelAction(getStartServices: MlCoreSetup['get i18n.translate('xpack.ml.actions.editSwimlaneTitle', { defaultMessage: 'Edit swim lane', }), - execute: async ({ embeddable }: EditSwimlanePanelContext) => { + async execute({ embeddable }: EditSwimlanePanelContext) { if (!embeddable) { throw new Error('Not possible to execute an action without the embeddable context'); } @@ -35,15 +31,19 @@ export function createEditSwimlanePanelAction(getStartServices: MlCoreSetup['get const [coreStart] = await getStartServices(); try { + const { resolveAnomalySwimlaneUserInput } = await import( + '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout' + ); + const result = await resolveAnomalySwimlaneUserInput(coreStart, embeddable.getInput()); embeddable.updateInput(result); } catch (e) { return Promise.reject(); } }, - isCompatible: async ({ embeddable }: EditSwimlanePanelContext) => { + async isCompatible({ embeddable }: EditSwimlanePanelContext) { return ( - embeddable instanceof AnomalySwimlaneEmbeddable && + embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE && embeddable.getInput().viewMode === ViewMode.EDIT ); }, diff --git a/x-pack/plugins/ml/public/ui_actions/index.ts b/x-pack/plugins/ml/public/ui_actions/index.ts index b7262a330b31..437a38acf6f8 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -13,7 +13,6 @@ import { createOpenInExplorerAction, OPEN_IN_ANOMALY_EXPLORER_ACTION, } from './open_in_anomaly_explorer_action'; -import { EditSwimlanePanelContext } from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; import { UiActionsSetup } from '../../../../../src/plugins/ui_actions/public'; import { MlPluginStart, MlStartDependencies } from '../plugin'; import { CONTEXT_MENU_TRIGGER } from '../../../../../src/plugins/embeddable/public'; @@ -22,11 +21,18 @@ import { createApplyInfluencerFiltersAction, } from './apply_influencer_filters_action'; import { SWIM_LANE_SELECTION_TRIGGER, swimLaneSelectionTrigger } from './triggers'; -import { SwimLaneDrilldownContext } from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; import { APPLY_TIME_RANGE_SELECTION_ACTION, createApplyTimeRangeSelectionAction, } from './apply_time_range_action'; +import { EditSwimlanePanelContext, SwimLaneDrilldownContext } from '../embeddables'; + +export { APPLY_TIME_RANGE_SELECTION_ACTION } from './apply_time_range_action'; +export { EDIT_SWIMLANE_PANEL_ACTION } from './edit_swimlane_panel_action'; +export { APPLY_INFLUENCER_FILTERS_ACTION } from './apply_influencer_filters_action'; +export { OPEN_IN_ANOMALY_EXPLORER_ACTION } from './open_in_anomaly_explorer_action'; + +export { SWIM_LANE_SELECTION_TRIGGER } from './triggers'; /** * Register ML UI actions diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index 211840467e38..e18f593145f9 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -6,12 +6,9 @@ import { i18n } from '@kbn/i18n'; import { ActionContextMapping, createAction } from '../../../../../src/plugins/ui_actions/public'; -import { - AnomalySwimlaneEmbeddable, - SwimLaneDrilldownContext, -} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; import { MlCoreSetup } from '../plugin'; import { ML_APP_URL_GENERATOR } from '../url_generator'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; export const OPEN_IN_ANOMALY_EXPLORER_ACTION = 'openInAnomalyExplorerAction'; @@ -60,7 +57,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta await application.navigateToUrl(anomalyExplorerUrl!); }, async isCompatible({ embeddable }: SwimLaneDrilldownContext) { - return embeddable instanceof AnomalySwimlaneEmbeddable; + return embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE; }, }); } diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts index b7cf64159a82..4e08c57c0b2e 100644 --- a/x-pack/plugins/ml/public/url_generator.ts +++ b/x-pack/plugins/ml/public/url_generator.ts @@ -5,13 +5,23 @@ */ import { CoreSetup } from 'kibana/public'; -import { SharePluginSetup, UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; +import { + SharePluginSetup, + UrlGeneratorsDefinition, + UrlGeneratorState, +} from '../../../../src/plugins/share/public'; import { TimeRange } from '../../../../src/plugins/data/public'; import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; import { JobId } from '../../reporting/common/types'; import { ExplorerAppState } from './application/explorer/explorer_dashboard_service'; import { MlStartDependencies } from './plugin'; +declare module '../../../../src/plugins/share/public' { + export interface UrlGeneratorStateMapping { + [ML_APP_URL_GENERATOR]: UrlGeneratorState; + } +} + export const ML_APP_URL_GENERATOR = 'ML_APP_URL_GENERATOR'; export interface ExplorerUrlState { diff --git a/x-pack/plugins/monitoring/common/enums.ts b/x-pack/plugins/monitoring/common/enums.ts index 74711b31756b..d4058e9de801 100644 --- a/x-pack/plugins/monitoring/common/enums.ts +++ b/x-pack/plugins/monitoring/common/enums.ts @@ -26,3 +26,8 @@ export enum AlertParamType { Duration = 'duration', Percentage = 'percentage', } + +export enum SetupModeFeature { + MetricbeatMigration = 'metricbeatMigration', + Alerts = 'alerts', +} diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 02963e9457ab..1d67eebb1705 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -180,7 +180,7 @@ export const AlertsBadge: React.FC = (props: Props) => { } return ( - + {badges.map((badge, index) => ( {badge} diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index 7754af1be858..6dcfa6dd043a 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -26,6 +26,8 @@ import { APM_SYSTEM_ID } from '../../../../common/constants'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { SetupModeBadge } from '../../setup_mode/badge'; import { FormattedMessage } from '@kbn/i18n/react'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; function getColumns(setupMode) { return [ @@ -36,7 +38,7 @@ function getColumns(setupMode) { field: 'name', render: (name, apm) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const status = list[apm.uuid] || {}; const instance = { @@ -129,7 +131,7 @@ export function ApmServerInstances({ apms, setupMode }) { const { pagination, sorting, onTableChange, data } = apms; let setupModeCallout = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallout = ( { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const status = list[beat.uuid] || {}; const instance = { @@ -122,7 +124,7 @@ export class Listing extends PureComponent { const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props; let setupModeCallOut = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallOut = ( getSafeForExternalLink('#/apm/instances'); const setupModeData = get(setupMode.data, 'apm'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; return ( - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index df0070176727..3591ad178f4c 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -25,6 +25,8 @@ import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { BEATS_SYSTEM_ID } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; export function BeatsPanel(props) { const { setupMode } = props; @@ -35,14 +37,15 @@ export function BeatsPanel(props) { } const setupModeData = get(setupMode.data, 'beats'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; const beatTypes = props.beats.types.map((beat, index) => { return [ @@ -142,7 +145,7 @@ export function BeatsPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {beatTypes} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index edf4c5d73f83..34e995510cf7 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -44,6 +44,8 @@ import { } from '../../../../common/constants'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { SetupModeFeature } from '../../../../common/enums'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; const calculateShards = (shards) => { const total = get(shards, 'total', 0); @@ -172,14 +174,15 @@ export function ElasticsearchPanel(props) { const { primaries, replicas } = calculateShards(get(props, 'cluster_stats.indices.shards', {})); const setupModeData = get(setupMode.data, 'elasticsearch'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; const showMlJobs = () => { // if license doesn't support ML, then `ml === null` @@ -367,7 +370,7 @@ export function ElasticsearchPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {nodesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index eb1f82eb5550..6fa533302db4 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -32,6 +32,8 @@ import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../com import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; @@ -50,14 +52,15 @@ export function KibanaPanel(props) { const goToInstances = () => getSafeForExternalLink('#/kibana/instances'); const setupModeData = get(setupMode.data, 'kibana'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; let instancesAlertStatus = null; if (shouldShowAlertBadge(alerts, INSTANCES_PANEL_ALERTS)) { @@ -165,7 +168,7 @@ export function KibanaPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {instancesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 7c9758bc0ddb..9b4a50271a24 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -37,6 +37,8 @@ import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; @@ -56,14 +58,15 @@ export function LogstashPanel(props) { const goToPipelines = () => getSafeForExternalLink('#/logstash/pipelines'); const setupModeData = get(setupMode.data, 'logstash'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; let nodesAlertStatus = null; if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS)) { @@ -162,7 +165,7 @@ export function LogstashPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {nodesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index b7463fe6532b..43512f8e528f 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -32,6 +32,8 @@ import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { AlertsStatus } from '../../../alerts/status'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const getNodeTooltip = (node) => { const { nodeTypeLabel, nodeTypeClass } = node; @@ -85,7 +87,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler ); let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = _.get(setupMode, 'data.byUuid', {}); const status = list[node.resolver] || {}; const instance = { @@ -309,7 +311,11 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear // Merge the nodes data with the setup data if enabled const nodes = props.nodes || []; - if (setupMode.enabled && setupMode.data) { + if ( + setupMode && + setupMode.enabled && + isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration) + ) { // We want to create a seamless experience for the user by merging in the setup data // and the node data from monitoring indices in the likely scenario where some nodes // are using MB collection and some are using no collection @@ -332,7 +338,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear } let setupModeCallout = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallout = ( { const columns = [ @@ -39,7 +41,7 @@ const getColumns = (setupMode, alerts) => { field: 'name', render: (name, kibana) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const uuid = get(kibana, 'kibana.uuid'); const status = list[uuid] || {}; @@ -166,7 +168,7 @@ export class KibanaInstances extends PureComponent { let setupModeCallOut = null; // Merge the instances data with the setup data if enabled const instances = this.props.instances || []; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { // We want to create a seamless experience for the user by merging in the setup data // and the node data from monitoring indices in the likely scenario where some instances // are using MB collection and some are using no collection diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index caa21e5e6929..4a1137079ebb 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -25,6 +25,8 @@ import { SetupModeBadge } from '../../setup_mode/badge'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsStatus } from '../../../alerts/status'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; export class Listing extends PureComponent { getColumns() { @@ -40,7 +42,7 @@ export class Listing extends PureComponent { sortable: true, render: (name, node) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const uuid = get(node, 'logstash.uuid'); const status = list[uuid] || {}; @@ -167,7 +169,7 @@ export class Listing extends PureComponent { })); let setupModeCallOut = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallOut = ( - + diff --git a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap index 2eaa25803c81..0d9e50d14657 100644 --- a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap +++ b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap @@ -3,6 +3,7 @@ exports[`EnterButton should render properly 1`] = `
= ( } return ( -
+
{tooltip}; + return ( + + {tooltip} + + ); } diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.js index cc58d7267f6d..44ee883c135d 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table.js @@ -8,6 +8,8 @@ import React, { Fragment } from 'react'; import { EuiInMemoryTable, EuiButton, EuiSpacer, EuiSearchBar } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; +import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; +import { SetupModeFeature } from '../../../common/enums'; export function EuiMonitoringTable({ rows: items, @@ -45,7 +47,7 @@ export function EuiMonitoringTable({ }); let footerContent = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { footerContent = ( diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js index 618547398bd9..9b4b086a0208 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js @@ -8,6 +8,8 @@ import React, { Fragment } from 'react'; import { EuiBasicTable, EuiSpacer, EuiSearchBar, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; +import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; +import { SetupModeFeature } from '../../../common/enums'; export function EuiMonitoringSSPTable({ rows: items, @@ -46,7 +48,11 @@ export function EuiMonitoringSSPTable({ }); let footerContent = null; - if (setupMode && setupMode.enabled) { + if ( + setupMode && + setupMode.enabled && + isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration) + ) { footerContent = ( diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js index eda32cd39c0d..d682e87b7ca9 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.js +++ b/x-pack/plugins/monitoring/public/directives/main/index.js @@ -11,9 +11,14 @@ import { get } from 'lodash'; import template from './index.html'; import { Legacy } from '../../legacy_shims'; import { shortenPipelineHash } from '../../../common/formatting'; -import { getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; +import { + getSetupModeState, + initSetupModeState, + isSetupModeFeatureEnabled, +} from '../../lib/setup_mode'; import { Subscription } from 'rxjs'; import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; +import { SetupModeFeature } from '../../../common/enums'; const setOptions = (controller) => { if ( @@ -179,7 +184,7 @@ export class MonitoringMainController { isDisabledTab(product) { const setupMode = getSetupModeState(); - if (!setupMode.enabled || !setupMode.data) { + if (!isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { return false; } diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index a36b945e82ef..b99093a3d8ad 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from '../legacy_shims'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; +import { SetupModeFeature } from '../../common/enums'; function isOnPage(hash: string) { return includes(window.location.hash, hash); @@ -93,16 +94,12 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; const hasPermissions = get(data, '_meta.hasPermissions', false); - if (Legacy.shims.isCloud || !hasPermissions) { + if (!hasPermissions) { let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { defaultMessage: 'You do not have the necessary permissions to do this.', }); - } else { - text = i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { - defaultMessage: 'This feature is not available on cloud.', - }); } angularState.scope.$evalAsync(() => { @@ -180,7 +177,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const enabled = !globalState.inSetupMode && !Legacy.shims.isCloud; + const enabled = !globalState.inSetupMode; render( , @@ -212,3 +209,15 @@ export const isInSetupMode = () => { const globalState = $injector.get('globalState'); return globalState.inSetupMode; }; + +export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { + if (!setupModeState.enabled) { + return false; + } + if (feature === SetupModeFeature.MetricbeatMigration) { + if (Legacy.shims.isCloud) { + return false; + } + } + return true; +}; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index cfac5e195a12..88b36b9572fc 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -128,7 +128,7 @@ export class MonitoringPlugin UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS, JSON.stringify(refreshInterval) ); - uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); + uiSettings.overrideLocalDefault(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS, JSON.stringify(time)); } private getExternalConfig() { diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 2f88245d88c4..a41d4ec4bbfa 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -11,7 +11,8 @@ import { getPageData } from '../lib/get_page_data'; import { PageLoading } from '../components'; import { Legacy } from '../legacy_shims'; import { PromiseWithCancel } from '../../common/cancel_promise'; -import { updateSetupModeData, getSetupModeState } from '../lib/setup_mode'; +import { SetupModeFeature } from '../../common/enums'; +import { updateSetupModeData, isSetupModeFeatureEnabled } from '../lib/setup_mode'; /** * Given a timezone, this function will calculate the offset in milliseconds @@ -150,11 +151,10 @@ export class MonitoringViewBaseController { } const _api = apiUrlFn ? apiUrlFn() : api; const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; - const setupMode = getSetupModeState(); if (alerts.shouldFetch) { promises.push(fetchAlerts()); } - if (setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { promises.push(updateSetupModeData()); } this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index cac57f599633..016acf2737f9 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -50,6 +50,7 @@ export class BaseAlert { protected getLogger!: (...scopes: string[]) => Logger; protected config!: MonitoringConfig; protected kibanaUrl!: string; + protected isCloud: boolean = false; protected defaultParams: CommonAlertParams | {} = {}; public get paramDetails() { return {}; @@ -82,13 +83,15 @@ export class BaseAlert { monitoringCluster: ILegacyCustomClusterClient, getLogger: (...scopes: string[]) => Logger, config: MonitoringConfig, - kibanaUrl: string + kibanaUrl: string, + isCloud: boolean ) { this.getUiSettingsService = getUiSettingsService; this.monitoringCluster = monitoringCluster; this.config = config; this.kibanaUrl = kibanaUrl; this.getLogger = getLogger; + this.isCloud = isCloud; } public getAlertType(): AlertType { diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index f25179fa63c2..4b083787f58c 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -112,7 +112,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -175,7 +176,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -223,7 +225,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index 2596252c92d1..c330e977e53d 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -116,7 +116,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -214,7 +215,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -286,7 +288,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -352,7 +355,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -436,7 +440,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -564,5 +569,34 @@ describe('CpuUsageAlert', () => { }, ]); }); + + it('should fire with different messaging for cloud', async () => { + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + true + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + action: `[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify CPU levels across affected nodes.', + clusterName, + count, + nodes: `${nodeName}:${cpuUsage.toFixed(2)}`, + state: 'firing', + }); + }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 4742f5548704..afe5abcf1ebd 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -322,29 +322,31 @@ export class CpuUsageAlert extends BaseAlert { ',' )})`; const action = `[${fullActionText}](${url})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + action, + }, + } + ); instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - action, - }, - } - ), + internalShortMessage, + internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage, state: FIRING, nodes: firingNodes, count: firingCount, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 50bf40825c51..ed300c211215 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -115,7 +115,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -166,7 +167,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -214,7 +216,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 1a76fae9fc42..dd3b37b5755e 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -118,7 +118,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -168,7 +169,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -216,7 +218,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 0f677dcc9c12..e2f21b34efe2 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -122,7 +122,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -195,7 +196,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -243,7 +245,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index f29c199b3f1e..fbb4a01d5b4e 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -115,7 +115,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -165,7 +166,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -213,7 +215,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index d45d404b3830..4b3e3d2d6cb6 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -128,7 +128,8 @@ describe('NodesChangedAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -180,7 +181,8 @@ describe('NodesChangedAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 86022a0e863d..ed091d4b8d7a 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -126,10 +126,17 @@ export class Plugin { const coreStart = (await core.getStartServices())[0]; return coreStart.uiSettings; }; - + const isCloud = Boolean(plugins.cloud?.isCloudEnabled); const alerts = AlertsFactory.getAll(); for (const alert of alerts) { - alert.initializeAlertType(getUiSettingsService, cluster, this.getLogger, config, kibanaUrl); + alert.initializeAlertType( + getUiSettingsService, + cluster, + this.getLogger, + config, + kibanaUrl, + isCloud + ); plugins.alerts.registerType(alert.getAlertType()); } diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 1e7a5acb3364..a0ef6d3e2d98 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -17,6 +17,7 @@ import { InfraPluginSetup } from '../../infra/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; +import { CloudSetup } from '../../cloud/server'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -44,6 +45,7 @@ export interface PluginsSetup { features: FeaturesPluginSetupContract; alerts: AlertingPluginSetupContract; infra: InfraPluginSetup; + cloud: CloudSetup; } export interface PluginsStart { diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 32bdb00577bd..8870bcbc9fa3 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -113,44 +113,46 @@ export function OverviewPage({ routeParams }: Props) { {/* Data sections */} {showDataSections && ( - - {hasData.infra_logs && ( - - - - )} - {hasData.infra_metrics && ( - - - - )} - {hasData.apm && ( - - - - )} - {hasData.uptime && ( - - - - )} - + + + {hasData.infra_logs && ( + + + + )} + {hasData.infra_metrics && ( + + + + )} + {hasData.apm && ( + + + + )} + {hasData.uptime && ( + + + + )} + + )} {/* Empty sections */} diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index eb16a9d6de1a..494f7ab0a28d 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -9,6 +9,7 @@ import { map, truncate } from 'lodash'; import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; +import { getDisallowedOutgoingUrlError } from '../'; import { LevelLogger } from '../../../lib'; import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ConditionalHeaders, ElementPosition } from '../../../types'; @@ -76,6 +77,9 @@ export class HeadlessChromiumDriver { }); } + /* + * Call Page.goto and wait to see the Kibana DOM content + */ public async open( url: string, { @@ -113,6 +117,16 @@ export class HeadlessChromiumDriver { logger.info(`handled ${this.interceptedCount} page requests`); } + /* + * Let modules poll if Chrome is still running so they can short circuit if needed + */ + public isPageOpen() { + return !this.page.isClosed(); + } + + /* + * Call Page.screenshot and return a base64-encoded string of the image + */ public async screenshot(elementPosition: ElementPosition): Promise { const { boundingClientRect, scroll } = elementPosition; const screenshot = await this.page.screenshot({ @@ -220,18 +234,13 @@ export class HeadlessChromiumDriver { // We should never ever let file protocol requests go through if (!allowed || !this.allowRequest(interceptedUrl)) { - logger.error(`Got bad URL: "${interceptedUrl}", closing browser.`); await client.send('Fetch.failRequest', { errorReason: 'Aborted', requestId, }); this.page.browser().close(); - throw new Error( - i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { - defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}", exiting`, - values: { interceptedUrl }, - }) - ); + logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + return; } if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, interceptedUrl)) { @@ -292,9 +301,9 @@ export class HeadlessChromiumDriver { } if (!allowed || !this.allowRequest(interceptedUrl)) { - logger.error(`Got disallowed URL "${interceptedUrl}", closing browser.`); this.page.browser().close(); - throw new Error(`Received disallowed URL in response: ${interceptedUrl}`); + logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + throw getDisallowedOutgoingUrlError(interceptedUrl); } }); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 157d109e9e27..809bfb57dd4f 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -19,6 +19,7 @@ import { import * as Rx from 'rxjs'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; +import { getChromiumDisconnectedError } from '../'; import { BROWSER_TYPE } from '../../../../common/constants'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; @@ -115,13 +116,19 @@ export class HeadlessChromiumDriverFactory { logger.debug(`Browser page driver created`); } catch (err) { - observer.error(new Error(`Error spawning Chromium browser: [${err}]`)); + observer.error(new Error(`Error spawning Chromium browser!`)); + observer.error(err); throw err; } const childProcess = { async kill() { - await browser.close(); + try { + await browser.close(); + } catch (err) { + // do not throw + logger.error(err); + } }, }; const { terminate$ } = safeChildProcess(logger, childProcess); @@ -167,7 +174,8 @@ export class HeadlessChromiumDriverFactory { // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. del(userDataDir, { force: true }).catch((error) => { - logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); + logger.error(`error deleting user data directory at [${userDataDir}]!`); + logger.error(error); }); }); }); @@ -219,7 +227,7 @@ export class HeadlessChromiumDriverFactory { mergeMap((err) => { return Rx.throwError( i18n.translate('xpack.reporting.browsers.chromium.errorDetected', { - defaultMessage: 'Reporting detected an error: {err}', + defaultMessage: 'Reporting encountered an error: {err}', values: { err: err.toString() }, }) ); @@ -230,7 +238,7 @@ export class HeadlessChromiumDriverFactory { mergeMap((err) => { return Rx.throwError( i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { - defaultMessage: `Reporting detected an error on the page: {err}`, + defaultMessage: `Reporting encountered an error on the page: {err}`, values: { err: err.toString() }, }) ); @@ -238,15 +246,7 @@ export class HeadlessChromiumDriverFactory { ); const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( - mergeMap(() => - Rx.throwError( - new Error( - i18n.translate('xpack.reporting.browsers.chromium.chromiumClosed', { - defaultMessage: `Reporting detected that Chromium has closed.`, - }) - ) - ) - ) + mergeMap(() => Rx.throwError(getChromiumDisconnectedError())) ); return Rx.merge(pageError$, uncaughtExceptionPageError$, browserDisconnect$); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/index.ts index cebcd228b01c..29eb51dff21d 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { BrowserDownload } from '../'; import { CaptureConfig } from '../../../server/types'; import { LevelLogger } from '../../lib'; @@ -15,3 +16,18 @@ export const chromium: BrowserDownload = { createDriverFactory: (binaryPath: string, captureConfig: CaptureConfig, logger: LevelLogger) => new HeadlessChromiumDriverFactory(binaryPath, captureConfig, logger), }; + +export const getChromiumDisconnectedError = () => + new Error( + i18n.translate('xpack.reporting.screencapture.browserWasClosed', { + defaultMessage: 'Browser was closed unexpectedly! Check the server logs for more info.', + }) + ); + +export const getDisallowedOutgoingUrlError = (interceptedUrl: string) => + new Error( + i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { + defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}". Failing the request and closing the browser.`, + values: { interceptedUrl }, + }) + ); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/index.js similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/index.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png b/x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png rename to x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index e7fb0c6e2cb9..9acfc6d8c608 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -105,9 +105,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory if (errPayload.statusCode === 404) { throw notFound(errPayload.message); } - if (err.stack) { - logger.error(err.stack); - } + logger.error(err); throw new Error(`Unable to create a job from saved object data! Error: ${err}`); }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index f2ce423566c4..2e0292e1f9ab 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -90,7 +90,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { logger.debug(`PDF buffer byte length: ${buffer?.byteLength || 0}`); tracker.endGetBuffer(); } catch (err) { - logger.error(`Could not generate the PDF buffer! ${err}`); + logger.error(`Could not generate the PDF buffer!`); + logger.error(err); } tracker.end(); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js index f9a9d9d85bfd..1042fd66abad 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js @@ -11,7 +11,7 @@ import Printer from 'pdfmake'; import xRegExp from 'xregexp'; import { i18n } from '@kbn/i18n'; -const assetPath = path.resolve(__dirname, 'assets'); +const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); const tableBorderWidth = 1; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts b/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts new file mode 100644 index 000000000000..1b5e73648c2f --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.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 { HeadlessChromiumDriver } from '../../browsers'; +import { getChromiumDisconnectedError } from '../../browsers/chromium'; + +/* + * Call this function within error-handling `catch` blocks while setup and wait + * for the Kibana URL to be ready for screenshot. This detects if a block of + * code threw an exception because the page is closed or crashed. + * + * Also call once after `setup$` fires in the screenshot pipeline + */ +export const checkPageIsOpen = (browser: HeadlessChromiumDriver) => { + if (!browser.isPageOpen()) { + throw getChromiumDisconnectedError(); + } +}; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 1b72be6c92f4..0ad41cd90485 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -281,7 +281,7 @@ describe('Screenshot Observable Pipeline', () => { `); }); - it('recovers if exit$ fires a timeout signal', async () => { + it('observes page exit', async () => { // mocks const mockGetCreatePage = (driver: HeadlessChromiumDriver) => jest @@ -311,38 +311,7 @@ describe('Screenshot Observable Pipeline', () => { }).toPromise(); }; - await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 200, - "left": 0, - "top": 0, - "width": 200, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": "Instant timeout has fired!", - "screenshots": Array [ - Object { - "base64EncodedData": "allyourBase64", - "description": undefined, - "title": undefined, - }, - ], - "timeRange": null, - }, - ] - `); + await expect(getScreenshot()).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); }); it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index ab4dabf9ed2c..c6d3d826c88f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -24,6 +24,7 @@ import { ScreenshotResults, ScreenshotsObservableFn, } from '../../types'; +import { checkPageIsOpen } from './check_browser_open'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; @@ -68,7 +69,6 @@ export function screenshotsObservableFactory( return Rx.from(urls).pipe( concatMap((url, index) => { const setup$: Rx.Observable = Rx.of(1).pipe( - takeUntil(exit$), mergeMap(() => { // If we're moving to another page in the app, we'll want to wait for the app to tell us // it's loaded the next page. @@ -117,14 +117,19 @@ export function screenshotsObservableFactory( })); }), catchError((err) => { + checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it + logger.error(err); return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); }) ); return setup$.pipe( + takeUntil(exit$), mergeMap( async (data: ScreenSetupData): Promise => { + checkPageIsOpen(driver); // re-check that the browser has not closed + const elements = data.elementsPositionAndAttributes ? data.elementsPositionAndAttributes : getDefaultElementPosition(layout.getViewport(1)); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index db10d96db226..08313e6892f3 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -129,6 +129,7 @@ export const createMockBrowserDriverFactory = async ( mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open; + mockBrowserDriver.isPageOpen = () => true; mockBrowserDriverFactory.createPage = opts.getCreatePage ? opts.getCreatePage(mockBrowserDriver) diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts index 62f923aa6d79..6e6e0f443015 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { EndpointDocGenerator } from '../generate_data'; -import { descriptiveName, isStart } from './event'; +import { descriptiveName, isProcessRunning } from './event'; import { ResolverEvent } from '../types'; describe('Generated documents', () => { @@ -42,52 +42,66 @@ describe('Generated documents', () => { }); }); - describe('Start events', () => { - it('is a start event when event.type is a string', () => { + describe('Process running events', () => { + it('is a running event when event.type is a string', () => { const event: ResolverEvent = generator.generateEvent({ eventType: 'start', }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is a start event when event.type is an array of strings', () => { + it('is a running event when event.type is an array of strings', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['start'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is a start event when event.type is an array of strings and contains start', () => { + it('is a running event when event.type is an array of strings and contains start', () => { let event: ResolverEvent = generator.generateEvent({ eventType: ['bogus', 'start', 'creation'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); event = generator.generateEvent({ eventType: ['start', 'bogus'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is not a start event when event.type is not start', () => { + it('is not a running event when event.type is only and end type', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['end'], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); }); - it('is not a start event when event.type is empty', () => { + it('is not a running event when event.type is empty', () => { const event: ResolverEvent = generator.generateEvent({ eventType: [], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); }); - it('is not a start event when event.type is bogus', () => { + it('is not a running event when event.type is bogus', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['bogus'], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); + }); + + it('is a running event when event.type contains info', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['info'], + }); + expect(isProcessRunning(event)).toBeTruthy(); + }); + + it('is a running event when event.type contains change', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['bogus', 'change'], + }); + expect(isProcessRunning(event)).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index 216b5cc02858..1168b5edb6ff 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -9,16 +9,26 @@ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEven return (event as LegacyEndpointEvent).endgame !== undefined; } -export function isStart(event: ResolverEvent): boolean { +export function isProcessRunning(event: ResolverEvent): boolean { if (isLegacyEvent(event)) { - return event.event?.type === 'process_start' || event.event?.action === 'fork_event'; + return ( + event.event?.type === 'process_start' || + event.event?.action === 'fork_event' || + event.event?.type === 'already_running' + ); } if (Array.isArray(event.event.type)) { - return event.event.type.includes('start'); + return ( + event.event.type.includes('start') || + event.event.type.includes('change') || + event.event.type.includes('info') + ); } - return event.event.type === 'start'; + return ( + event.event.type === 'start' || event.event.type === 'change' || event.event.type === 'info' + ); } export function eventTimestamp(event: ResolverEvent): string | undefined | number { diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts new file mode 100644 index 000000000000..10f9ebb5623d --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { newThresholdRule } from '../objects/rule'; + +import { + CUSTOM_RULES_BTN, + RISK_SCORE, + RULE_NAME, + RULES_ROW, + RULES_TABLE, + SEVERITY, +} from '../screens/alerts_detection_rules'; +import { + ABOUT_FALSE_POSITIVES, + ABOUT_INVESTIGATION_NOTES, + ABOUT_MITRE, + ABOUT_RISK, + ABOUT_RULE_DESCRIPTION, + ABOUT_SEVERITY, + ABOUT_STEP, + ABOUT_TAGS, + ABOUT_URLS, + DEFINITION_CUSTOM_QUERY, + DEFINITION_INDEX_PATTERNS, + DEFINITION_THRESHOLD, + DEFINITION_TIMELINE, + DEFINITION_STEP, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + RULE_ABOUT_DETAILS_HEADER_TOGGLE, + RULE_NAME_HEADER, + SCHEDULE_LOOPBACK, + SCHEDULE_RUNS, + SCHEDULE_STEP, +} from '../screens/rule_details'; + +import { + goToManageAlertsDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../tasks/alerts'; +import { + changeToThreeHundredRowsPerPage, + filterByCustomRules, + goToCreateNewRule, + goToRuleDetails, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/alerts_detection_rules'; +import { + createAndActivateRule, + fillAboutRuleAndContinue, + fillDefineThresholdRuleAndContinue, + selectThresholdRuleType, +} from '../tasks/create_new_rule'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS_URL } from '../urls/navigation'; + +describe('Detection rules, threshold', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Creates and activates a new threshold rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRuleAndContinue(newThresholdRule); + fillAboutRuleAndContinue(newThresholdRule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 1; + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + + filterByCustomRules(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); + }); + cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name); + cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + let expectedUrls = ''; + newThresholdRule.referenceUrls.forEach((url) => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newThresholdRule.falsePositivesExamples.forEach((falsePositive) => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newThresholdRule.tags.forEach((tag) => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newThresholdRule.mitre.forEach((mitre) => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach((technique) => { + expectedMitre = expectedMitre + technique; + }); + }); + const expectedIndexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ]; + + cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newThresholdRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newThresholdRule.description); + cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); + cy.get(ABOUT_STEP) + .eq(ABOUT_FALSE_POSITIVES) + .invoke('text') + .should('eql', expectedFalsePositives); + cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); + cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); + + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newThresholdRule.customQuery} `); + cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_THRESHOLD) + .invoke('text') + .should( + 'eql', + `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` + ); + + cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); + cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index a30fddc3c3a6..aeadc34c6e49 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -33,6 +33,11 @@ export interface CustomRule { timelineId: string; } +export interface ThresholdRule extends CustomRule { + thresholdField: string; + threshold: string; +} + export interface MachineLearningRule { machineLearningJob: string; anomalyScoreThreshold: string; @@ -72,6 +77,22 @@ export const newRule: CustomRule = { timelineId: '0162c130-78be-11ea-9718-118a926974a4', }; +export const newThresholdRule: ThresholdRule = { + customQuery: 'host.name:*', + name: 'New Rule Test', + description: 'The new rule description.', + severity: 'High', + riskScore: '17', + tags: ['test', 'newRule'], + referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [mitre1, mitre2], + note: '# test markdown', + timelineId: '0162c130-78be-11ea-9718-118a926974a4', + thresholdField: 'host.name', + threshold: '10', +}; + export const machineLearningRule: MachineLearningRule = { machineLearningJob: 'linux_anomalous_network_service', anomalyScoreThreshold: '20', diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index bc0740554bc5..af4fe7257ae5 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -27,6 +27,8 @@ export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; +export const INPUT = '[data-test-subj="input"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; @@ -64,3 +66,9 @@ export const SEVERITY_DROPDOWN = export const TAGS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxSearchInput"]'; + +export const THRESHOLD_FIELD_SELECTION = '.euiFilterSelectItem'; + +export const THRESHOLD_INPUT_AREA = '[data-test-subj="thresholdInput"]'; + +export const THRESHOLD_TYPE = '[data-test-subj="thresholdRuleType"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index ec57e142125d..1c0102382ab6 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -26,6 +26,8 @@ export const ANOMALY_SCORE = 1; export const DEFINITION_CUSTOM_QUERY = 1; +export const DEFINITION_THRESHOLD = 4; + export const DEFINITION_TIMELINE = 3; export const DEFINITION_INDEX_PATTERNS = diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index 789759643e31..4f382f13bcd5 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -32,7 +32,6 @@ Cypress.Commands.add('stubSecurityApi', function (dataFileName) { cy.on('window:before:load', (win) => { - // @ts-ignore no null, this is a temp hack see issue above win.fetch = null; }); cy.server(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 88ae582b5889..de9d343bc91f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -3,7 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CustomRule, MachineLearningRule, machineLearningRule } from '../objects/rule'; + +import { + CustomRule, + MachineLearningRule, + machineLearningRule, + ThresholdRule, +} from '../objects/rule'; import { ABOUT_CONTINUE_BTN, ANOMALY_THRESHOLD_INPUT, @@ -15,6 +21,7 @@ import { DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, + INPUT, INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, @@ -30,6 +37,9 @@ import { SCHEDULE_CONTINUE_BUTTON, SEVERITY_DROPDOWN, TAGS_INPUT, + THRESHOLD_FIELD_SELECTION, + THRESHOLD_INPUT_AREA, + THRESHOLD_TYPE, } from '../screens/create_new_rule'; import { TIMELINE } from '../screens/timeline'; @@ -39,7 +49,9 @@ export const createAndActivateRule = () => { cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist'); }; -export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) => { +export const fillAboutRuleAndContinue = ( + rule: CustomRule | MachineLearningRule | ThresholdRule +) => { cy.get(RULE_NAME_INPUT).type(rule.name, { force: true }); cy.get(RULE_DESCRIPTION_INPUT).type(rule.description, { force: true }); @@ -80,18 +92,28 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); }; -export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { - cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); +export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timelineId)).click(); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; -export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { - cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); - cy.get(TIMELINE(rule.timelineId)).click(); +export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { + const thresholdField = 0; + const threshold = 1; + + cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); + cy.get(THRESHOLD_INPUT_AREA) + .find(INPUT) + .then((inputs) => { + cy.wrap(inputs[thresholdField]).type(rule.thresholdField); + cy.get(THRESHOLD_FIELD_SELECTION).click({ force: true }); + cy.wrap(inputs[threshold]).clear().type(rule.threshold); + }); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -111,3 +133,7 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu export const selectMachineLearningRuleType = () => { cy.get(MACHINE_LEARNING_TYPE).click({ force: true }); }; + +export const selectThresholdRuleType = () => { + cy.get(THRESHOLD_TYPE).click({ force: true }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts index 809498d25c5d..2e1d3379dc20 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts @@ -38,7 +38,6 @@ export const setTimelineEndDate = (date: string) => { cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { - // @ts-ignore if (Cypress.dom.isAttached($el)) { cy.wrap($el).click({ force: true }); } @@ -55,7 +54,6 @@ export const setTimelineStartDate = (date: string) => { cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { - // @ts-ignore if (Cypress.dom.isAttached($el)) { cy.wrap($el).click({ force: true }); } diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 41b9252c67b8..7c287646ba7a 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -7,7 +7,6 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { useThrottledResizeObserver } from '../../common/components/utils'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout } from '../../timelines/components/flyout'; import { HeaderGlobal } from '../../common/components/header_global'; @@ -19,43 +18,28 @@ import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; -const WrappedByAutoSizer = styled.div` +const SecuritySolutionAppWrapper = styled.div` + display: flex; + flex-direction: column; height: 100%; + width: 100%; `; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; const Main = styled.main` - height: 100%; + overflow: auto; + flex: 1; `; + Main.displayName = 'Main'; const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) -/** the global Kibana navigation at the top of every page */ -export const globalHeaderHeightPx = 48; - -const calculateFlyoutHeight = ({ - globalHeaderSize, - windowHeight, -}: { - globalHeaderSize: number; - windowHeight: number; -}): number => Math.max(0, windowHeight - globalHeaderSize); - interface HomePageProps { children: React.ReactNode; } -export const HomePage: React.FC = ({ children }) => { - const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); - const flyoutHeight = useMemo( - () => - calculateFlyoutHeight({ - globalHeaderSize: globalHeaderHeightPx, - windowHeight, - }), - [windowHeight] - ); +const HomePageComponent: React.FC = ({ children }) => { const { signalIndexExists, signalIndexName } = useSignalIndex(); const indexToAdd = useMemo(() => { @@ -69,7 +53,7 @@ export const HomePage: React.FC = ({ children }) => { const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); return ( - +
@@ -78,11 +62,7 @@ export const HomePage: React.FC = ({ children }) => { {indicesExist && showTimeline && ( <> - + )} @@ -91,8 +71,10 @@ export const HomePage: React.FC = ({ children }) => {
-
+ ); }; -HomePage.displayName = 'HomePage'; +HomePageComponent.displayName = 'HomePage'; + +export const HomePage = React.memo(HomePageComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 88969c3ae5fb..f697ce443f2c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { AddComment } from '.'; +import { AddComment, AddCommentRefObject } from '.'; import { TestProviders } from '../../../common/mock'; import { getFormMock } from '../__mock__/form'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; @@ -60,9 +60,11 @@ const defaultPostCommment = { isError: false, postComment, }; + const sampleData = { comment: 'what a cool comment', }; + describe('AddComment ', () => { const formHookMock = getFormMock(sampleData); @@ -122,16 +124,18 @@ describe('AddComment ', () => { ).toBeTruthy(); }); - it('should insert a quote if one is available', () => { + it('should insert a quote', () => { const sampleQuote = 'what a cool quote'; + const ref = React.createRef(); mount( - + ); + ref.current!.addQuote(sampleQuote); expect(formHookMock.setFieldValue).toBeCalledWith( 'comment', `${sampleData.comment}\n\n${sampleQuote}` diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index a54cf142c18b..87bd7bb24705 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; import { CommentRequest } from '../../../../../case/common/api'; @@ -30,88 +30,98 @@ const initialCommentValue: CommentRequest = { comment: '', }; +export interface AddCommentRefObject { + addQuote: (quote: string) => void; +} + interface AddCommentProps { caseId: string; disabled?: boolean; - insertQuote: string | null; onCommentSaving?: () => void; onCommentPosted: (newCase: Case) => void; showLoading?: boolean; } -export const AddComment = React.memo( - ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { - const { isLoading, postComment } = usePostComment(caseId); - const { form } = useForm({ - defaultValue: initialCommentValue, - options: { stripEmptyFields: false }, - schema, - }); - const { getFormData, setFieldValue, reset, submit } = form; - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'comment' - ); +export const AddComment = React.memo( + forwardRef( + ({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => { + const { isLoading, postComment } = usePostComment(caseId); + const { form } = useForm({ + defaultValue: initialCommentValue, + options: { stripEmptyFields: false }, + schema, + }); + const { getFormData, setFieldValue, reset, submit } = form; + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + form, + 'comment' + ); + + const addQuote = useCallback( + (quote) => { + const { comment } = getFormData(); + setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); + }, + [getFormData, setFieldValue] + ); - useEffect(() => { - if (insertQuote !== null) { - const { comment } = getFormData(); - setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}`); - } - }, [getFormData, insertQuote, setFieldValue]); + useImperativeHandle(ref, () => ({ + addQuote, + })); - const handleTimelineClick = useTimelineClick(); + const handleTimelineClick = useTimelineClick(); - const onSubmit = useCallback(async () => { - const { isValid, data } = await submit(); - if (isValid) { - if (onCommentSaving != null) { - onCommentSaving(); + const onSubmit = useCallback(async () => { + const { isValid, data } = await submit(); + if (isValid) { + if (onCommentSaving != null) { + onCommentSaving(); + } + postComment(data, onCommentPosted); + reset(); } - postComment(data, onCommentPosted); - reset(); - } - }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); + }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); - return ( - - {isLoading && showLoading && } - - - {i18n.ADD_COMMENT} -
- ), - topRightContent: ( - - ), - }} - /> - - - ); - } + return ( + + {isLoading && showLoading && } +
+ + {i18n.ADD_COMMENT} + + ), + topRightContent: ( + + ), + }} + /> + +
+ ); + } + ) ); AddComment.displayName = 'AddComment'; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 0c1da8694bf1..733e3db3c25e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -14,7 +14,7 @@ import * as i18n from '../case_view/translations'; import { Case, CaseUserActions } from '../../containers/types'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../../common/lib/kibana'; -import { AddComment } from '../add_comment'; +import { AddComment, AddCommentRefObject } from '../add_comment'; import { getLabelTitle } from './helpers'; import { UserActionItem } from './user_action_item'; import { UserActionMarkdown } from './user_action_markdown'; @@ -58,12 +58,12 @@ export const UserActionTree = React.memo( }: UserActionTreeProps) => { const { commentId } = useParams(); const handlerTimeoutId = useRef(0); + const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); const { isLoadingIds, patchComment } = useUpdateComment(); const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); - const [insertQuote, setInsertQuote] = useState(null); const handleManageMarkdownEditId = useCallback( (id: string) => { if (!manageMarkdownEditIds.includes(id)) { @@ -111,14 +111,17 @@ export const UserActionTree = React.memo( window.clearTimeout(handlerTimeoutId.current); }, 2400); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [handlerTimeoutId.current] + [handlerTimeoutId] ); const handleManageQuote = useCallback( (quote: string) => { const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); - setInsertQuote(`> ${addCarrots} \n`); + + if (addCommentRef && addCommentRef.current) { + addCommentRef.current.addQuote(`> ${addCarrots} \n`); + } + handleOutlineComment('add-comment'); }, [handleOutlineComment] @@ -152,14 +155,13 @@ export const UserActionTree = React.memo( ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [caseData.id, handleUpdate, insertQuote, userCanCrud] + [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId] ); useEffect(() => { @@ -169,8 +171,7 @@ export const UserActionTree = React.memo( handleOutlineComment(commentId); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [commentId, initLoading, isLoadingUserActions, isLoadingIds]); + }, [commentId, initLoading, isLoadingUserActions, isLoadingIds, handleOutlineComment]); return ( <> => { - const response = await KibanaServices.get().http.fetch( - `${ACTION_URL}/action/${connectorId}/_execute`, - { - method: 'POST', - body: JSON.stringify({ - params: { subAction: 'pushToService', subActionParams: casePushParams }, - }), - signal, - } - ); + const response = await KibanaServices.get().http.fetch< + ActionTypeExecutorResult> + >(`${ACTION_URL}/action/${connectorId}/_execute`, { + method: 'POST', + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), + signal, + }); if (response.status === 'error') { throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index e30560f6c814..841a1ef09ede 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -58,7 +58,6 @@ const defaultAlertsFilters: Filter[] = [ interface Props { timelineId: TimelineIdLiteral; endDate: string; - eventsViewerBodyHeight?: number; startDate: string; pageFilters?: Filter[]; } @@ -66,7 +65,6 @@ interface Props { const AlertsTableComponent: React.FC = ({ timelineId, endDate, - eventsViewerBodyHeight, startDate, pageFilters = [], }) => { @@ -93,7 +91,6 @@ const AlertsTableComponent: React.FC = ({ pageFilters={alertsFilter} defaultModel={alertsDefaultModel} end={endDate} - height={eventsViewerBodyHeight} id={timelineId} start={startDate} /> diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 832b14f00159..633135d63ac3 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -5,17 +5,9 @@ */ import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; -import { useWindowSize } from 'react-use'; -import { globalHeaderHeightPx } from '../../../app/home'; -import { DEFAULT_NUMBER_FORMAT, FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { useFullScreen } from '../../containers/use_full_screen'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../events_viewer/events_viewer'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; @@ -45,7 +37,6 @@ export const AlertsView = ({ // eslint-disable-next-line react-hooks/exhaustive-deps [] ); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( () => ({ @@ -79,17 +70,6 @@ export const AlertsView = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 8617388f4ffb..64c8fde87a6b 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -356,6 +356,71 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { }); }); +describe.each(chartDataSets)('BarChart with custom color', () => { + let wrapper: ReactWrapper; + + const data = [ + { + key: 'python.exe', + value: [ + { + x: 1586754900000, + y: 9675, + g: 'python.exe', + }, + ], + color: '#1EA591', + }, + { + key: 'kernel', + value: [ + { + x: 1586754900000, + y: 8708, + g: 'kernel', + }, + { + x: 1586757600000, + y: 9282, + g: 'kernel', + }, + ], + color: '#000000', + }, + { + key: 'sshd', + value: [ + { + x: 1586754900000, + y: 5907, + g: 'sshd', + }, + ], + color: '#ffffff', + }, + ]; + + const expectedColors = ['#1EA591', '#000000', '#ffffff']; + + const stackByField = 'process.name'; + + beforeAll(() => { + wrapper = mount( + + + + + + ); + }); + + expectedColors.forEach((color, i) => { + test(`it renders the expected legend color ${color} for legend item ${i}`, () => { + expect(wrapper.find(`div [color="${color}"]`).exists()).toBe(true); + }); + }); +}); + describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', (data) => { let shallowWrapper: ShallowWrapper; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx index fba8c3faa923..cafb0095431f 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx @@ -133,7 +133,7 @@ export const BarChartComponent: React.FC = ({ () => barChart != null && stackByField != null ? barChart.map((d, i) => ({ - color: d.color ?? i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + color: d.color ?? (i < defaultLegendColors.length ? defaultLegendColors[i] : undefined), dataProviderId: escapeDataProviderId( `draggable-legend-item-${uuid.v4()}-${stackByField}-${d.key}` ), diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index af40d4ff18d5..00a4e581320b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -55,7 +55,7 @@ export const EventFieldsBrowser = React.memo( return (
, column `render` callbacks expect complete BrowserField + // @ts-expect-error items going in match Partial, column `render` callbacks expect complete BrowserField items={items} columns={columns} pagination={false} 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 index e836e2e20432..436386077e72 100644 --- 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 @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { BrowserFields, DocValueFields } from '../../containers/source'; @@ -50,18 +50,18 @@ const TitleText = styled.span` margin-right: 12px; `; -const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; - const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` + display: flex; + flex-direction: column; + ${({ $isFullScreen }) => $isFullScreen && - css` + ` border: 0; box-shadow: none; padding-top: 0; padding-bottom: 0; - `} - max-width: 100%; + `} `; const TitleFlexGroup = styled(EuiFlexGroup)` @@ -70,7 +70,10 @@ const TitleFlexGroup = styled(EuiFlexGroup)` const EventsContainerLoading = styled.div` width: 100%; - overflow: auto; + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; `; /** @@ -78,9 +81,7 @@ const EventsContainerLoading = styled.div` * from being unmounted, to preserve the state of the component */ const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` - ${({ show }) => css` - ${show ? '' : 'visibility: hidden;'}; - `} + ${({ show }) => (show ? '' : 'visibility: hidden;')} `; interface Props { @@ -119,7 +120,6 @@ const EventsViewerComponent: React.FC = ({ end, filters, headerFilterGroup, - height = DEFAULT_EVENTS_VIEWER_HEIGHT, id, indexPattern, isLive, @@ -277,7 +277,6 @@ const EventsViewerComponent: React.FC = ({ docValueFields={docValueFields} id={id} isEventViewer={true} - height={height} sort={sort} toggleColumn={toggleColumn} /> @@ -326,7 +325,6 @@ export const EventsViewer = React.memo( prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && prevProps.headerFilterGroup === nextProps.headerFilterGroup && - prevProps.height === nextProps.height && prevProps.id === nextProps.id && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && prevProps.isLive === nextProps.isLive && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index c402116ee271..e4520dab4626 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import styled from 'styled-components'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -23,12 +24,20 @@ import { useUiSetting } from '../../lib/kibana'; import { EventsViewer } from './events_viewer'; import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { InspectButtonContainer } from '../inspect'; +import { useFullScreen } from '../../containers/use_full_screen'; + +const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; + +const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` + height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : `${DEFAULT_EVENTS_VIEWER_HEIGHT}px`)}; + display: flex; + width: 100%; +`; export interface OwnProps { defaultIndices?: string[]; defaultModel: SubsetTimelineModel; end: string; - height?: number; id: string; start: string; headerFilterGroup?: React.ReactNode; @@ -49,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC = ({ excludedRowRendererIds, filters, headerFilterGroup, - height, id, isLive, itemsPerPage, @@ -74,6 +82,8 @@ const StatefulEventsViewerComponent: React.FC = ({ 'events_viewer' ); + const { globalFullScreen } = useFullScreen(); + useEffect(() => { if (createTimeline != null) { createTimeline({ @@ -121,33 +131,34 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); return ( - - - + + + + + ); }; @@ -209,7 +220,6 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, - // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && @@ -219,7 +229,6 @@ export const StatefulEventsViewer = connector( prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && - prevProps.height === nextProps.height && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index e6eaa4947e40..7526c52d16fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -31,7 +31,7 @@ import * as i18n from './translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { ExceptionBuilder } from '../builder'; +import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; @@ -317,7 +317,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ {i18n.EXCEPTION_BUILDER_INFO} - { + test('it renders exceptionItemEntryFirstRowAndBadge for very first exception item in builder', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders exceptionItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders regular "and" badge if exception item is not the first one and includes more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx new file mode 100644 index 000000000000..3ce2f704b364 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx @@ -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 React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { AndOrBadge } from '../../and_or_badge'; + +const MyInvisibleAndBadge = styled(EuiFlexItem)` + visibility: hidden; +`; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface BuilderAndBadgeProps { + entriesLength: number; + exceptionItemIndex: number; +} + +export const BuilderAndBadgeComponent = React.memo( + ({ entriesLength, exceptionItemIndex }) => { + const badge = ; + + if (entriesLength > 1 && exceptionItemIndex === 0) { + return ( + + {badge} + + ); + } else if (entriesLength <= 1) { + return ( + + {badge} + + ); + } else { + return ( + + {badge} + + ); + } + } +); + +BuilderAndBadgeComponent.displayName = 'BuilderAndBadge'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx new file mode 100644 index 000000000000..b766a0536d23 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; + +import { BuilderEntryDeleteButtonComponent } from './entry_delete_button'; + +describe('BuilderEntryDeleteButtonComponent', () => { + test('it renders firstRowBuilderDeleteButton for very first entry in builder', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if entryIndex is not 0', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if exceptionItemIndex is not 0', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if nestedParentIndex is not null', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it invokes "onDelete" when button is clicked', () => { + const onDelete = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="builderDeleteButton"] button').simulate('click'); + + expect(onDelete).toHaveBeenCalledTimes(1); + expect(onDelete).toHaveBeenCalledWith(0, null); + }); + + test('it disables button if it is the only entry left and no field has been selected', () => { + const exceptionItem = { + ...getExceptionListItemSchemaMock(), + entries: [{ ...getEntryMatchMock(), field: '' }], + }; + const wrapper = mount( + + ); + + const button = wrapper.find('[data-test-subj="builderDeleteButton"] button').at(0); + + expect(button.prop('disabled')).toBeTruthy(); + }); + + test('it does not disable button if it is the only entry left and field has been selected', () => { + const wrapper = mount( + + ); + + const button = wrapper.find('[data-test-subj="builderDeleteButton"] button').at(0); + + expect(button.prop('disabled')).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx new file mode 100644 index 000000000000..e63f95064cba --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { BuilderEntry } from '../types'; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface BuilderEntryDeleteButtonProps { + entries: BuilderEntry[]; + isOnlyItem: boolean; + entryIndex: number; + exceptionItemIndex: number; + nestedParentIndex: number | null; + onDelete: (item: number, parent: number | null) => void; +} + +export const BuilderEntryDeleteButtonComponent = React.memo( + ({ entries, nestedParentIndex, isOnlyItem, entryIndex, exceptionItemIndex, onDelete }) => { + const isDisabled: boolean = + isOnlyItem && + entries.length === 1 && + exceptionItemIndex === 0 && + (entries[0].field == null || entries[0].field === ''); + + const handleDelete = useCallback((): void => { + onDelete(entryIndex, nestedParentIndex); + }, [onDelete, entryIndex, nestedParentIndex]); + + const button = ( + + ); + + if (entryIndex === 0 && exceptionItemIndex === 0 && nestedParentIndex == null) { + // This logic was added to work around it including the field + // labels in centering the delete icon for the first row + return ( + + {button} + + ); + } else { + return ( + + {button} + + ); + } + } +); + +BuilderEntryDeleteButtonComponent.displayName = 'BuilderEntryDeleteButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx index 0f54ec29cc54..2a116c4cd8ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { BuilderEntryItem } from './builder_entry_item'; +import { BuilderEntryItem } from './entry_item'; import { isOperator, isNotOperator, @@ -64,7 +64,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -91,7 +90,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -122,7 +120,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -155,7 +152,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -188,7 +184,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -221,7 +216,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -254,7 +248,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -287,7 +280,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -323,7 +315,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -377,7 +368,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -416,7 +406,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -451,7 +440,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -486,7 +474,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -521,7 +508,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -556,7 +542,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx index 3883a2fad2cf..3044f6d01b74 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -34,7 +34,6 @@ interface EntryItemProps { indexPattern: IIndexPattern; showLabel: boolean; listType: ExceptionListType; - addNested: boolean; onChange: (arg: BuilderEntry, i: number) => void; onlyShowListOperators?: boolean; } @@ -43,7 +42,6 @@ export const BuilderEntryItem: React.FC = ({ entry, indexPattern, listType, - addNested, showLabel, onChange, onlyShowListOperators = false, @@ -51,7 +49,6 @@ export const BuilderEntryItem: React.FC = ({ const handleFieldChange = useCallback( ([newField]: IFieldType[]): void => { const { updatedEntry, index } = getEntryOnFieldChange(entry, newField); - onChange(updatedEntry, index); }, [onChange, entry] diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx similarity index 84% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx index 7624ce147abd..e90639a2c028 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx @@ -15,11 +15,11 @@ import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/s import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; -import { ExceptionListItemComponent } from './builder_exception_item'; +import { BuilderExceptionListItemComponent } from './exception_item'; jest.mock('../../../../common/lib/kibana'); -describe('ExceptionListItemComponent', () => { +describe('BuilderExceptionListItemComponent', () => { const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); beforeAll(() => { @@ -46,7 +46,7 @@ describe('ExceptionListItemComponent', () => { }; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -71,11 +70,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders "and" badge when more than one exception item entry exists and it is not the first exception item', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -98,11 +96,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -127,11 +124,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders no "and" badge when "andLogicIncluded" is "false"', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={false} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -167,7 +163,7 @@ describe('ExceptionListItemComponent', () => { entries: [{ ...getEntryMatchMock(), field: '' }], }; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeTruthy(); }); test('it does not render delete button disabled when it is not the only entry left in builder', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeFalsy(); }); test('it does not render delete button disabled when "exceptionItemIndex" is not "0"', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - { // this to be true, but done for testing purposes isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeFalsy(); }); test('it does not render delete button disabled when more than one entry exists', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').at(0).props() + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').at(0).props() .disabled ).toBeFalsy(); }); test('it invokes "onChangeExceptionItem" when delete button clicked', () => { const mockOnDeleteExceptionItem = jest.fn(); - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchAnyMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={mockOnDeleteExceptionItem} onChangeExceptionItem={jest.fn()} /> ); wrapper - .find('[data-test-subj="exceptionItemEntryDeleteButton"] button') + .find('[data-test-subj="builderItemEntryDeleteButton"] button') .at(0) .simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx index 50a615083379..cd8b66acd223 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx @@ -5,23 +5,16 @@ */ import React, { useMemo, useCallback } from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { AndOrBadge } from '../../and_or_badge'; -import { BuilderEntryItem } from './builder_entry_item'; import { getFormattedBuilderEntries, getUpdatedEntriesOnDelete } from './helpers'; import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types'; import { ExceptionListType } from '../../../../../public/lists_plugin_deps'; - -const MyInvisibleAndBadge = styled(EuiFlexItem)` - visibility: hidden; -`; - -const MyFirstRowContainer = styled(EuiFlexItem)` - padding-top: 20px; -`; +import { BuilderEntryItem } from './entry_item'; +import { BuilderEntryDeleteButtonComponent } from './entry_delete_button'; +import { BuilderAndBadgeComponent } from './and_badge'; const MyBeautifulLine = styled(EuiFlexItem)` &:after { @@ -33,7 +26,7 @@ const MyBeautifulLine = styled(EuiFlexItem)` } `; -interface ExceptionListItemProps { +interface BuilderExceptionListItemProps { exceptionItem: ExceptionsBuilderExceptionItem; exceptionId: string; exceptionItemIndex: number; @@ -41,13 +34,12 @@ interface ExceptionListItemProps { andLogicIncluded: boolean; isOnlyItem: boolean; listType: ExceptionListType; - addNested: boolean; onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onlyShowListOperators?: boolean; } -export const ExceptionListItemComponent = React.memo( +export const BuilderExceptionListItemComponent = React.memo( ({ exceptionItem, exceptionId, @@ -55,7 +47,6 @@ export const ExceptionListItemComponent = React.memo( indexPattern, isOnlyItem, listType, - addNested, andLogicIncluded, onDeleteExceptionItem, onChangeExceptionItem, @@ -81,8 +72,8 @@ export const ExceptionListItemComponent = React.memo( (entryIndex: number, parentIndex: number | null): void => { const updatedExceptionItem = getUpdatedEntriesOnDelete( exceptionItem, - parentIndex ? parentIndex : entryIndex, - parentIndex ? entryIndex : null + entryIndex, + parentIndex ); onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex); @@ -98,63 +89,15 @@ export const ExceptionListItemComponent = React.memo( [exceptionItem.entries, indexPattern] ); - const getAndBadge = useCallback((): JSX.Element => { - const badge = ; - - if (andLogicIncluded && exceptionItem.entries.length > 1 && exceptionItemIndex === 0) { - return ( - - {badge} - - ); - } else if (andLogicIncluded && exceptionItem.entries.length <= 1) { - return ( - - {badge} - - ); - } else if (andLogicIncluded && exceptionItem.entries.length > 1) { - return ( - - {badge} - - ); - } else { - return <>; - } - }, [exceptionItem.entries.length, exceptionItemIndex, andLogicIncluded]); - - const getDeleteButton = useCallback( - (entryIndex: number, parentIndex: number | null): JSX.Element => { - const button = ( - handleDeleteEntry(entryIndex, parentIndex)} - isDisabled={ - isOnlyItem && - exceptionItem.entries.length === 1 && - exceptionItemIndex === 0 && - (exceptionItem.entries[0].field == null || exceptionItem.entries[0].field === '') - } - aria-label="entryDeleteButton" - className="exceptionItemEntryDeleteButton" - data-test-subj="exceptionItemEntryDeleteButton" - /> - ); - if (entryIndex === 0 && exceptionItemIndex === 0 && parentIndex == null) { - return {button}; - } else { - return {button}; - } - }, - [exceptionItemIndex, exceptionItem.entries, handleDeleteEntry, isOnlyItem] - ); - return ( - {getAndBadge()} + {andLogicIncluded && ( + + )} {entries.map((item, index) => ( @@ -166,7 +109,6 @@ export const ExceptionListItemComponent = React.memo( entry={item} indexPattern={indexPattern} listType={listType} - addNested={addNested} showLabel={ exceptionItemIndex === 0 && index === 0 && item.nested !== 'child' } @@ -174,10 +116,14 @@ export const ExceptionListItemComponent = React.memo( onlyShowListOperators={onlyShowListOperators} /> - {getDeleteButton( - item.entryIndex, - item.parent != null ? item.parent.parentIndex : null - )} + ))} @@ -189,4 +135,4 @@ export const ExceptionListItemComponent = React.memo( } ); -ExceptionListItemComponent.displayName = 'ExceptionListItem'; +BuilderExceptionListItemComponent.displayName = 'BuilderExceptionListItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 224c99756eb5..a3c5d09a0fb6 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -654,7 +654,7 @@ describe('Exception builder helpers', () => { expect(output).toEqual(expected); }); - test('it removes entry corresponding to "nestedEntryIndex"', () => { + test('it removes nested entry of "entryIndex" with corresponding parent index', () => { const payloadItem: ExceptionsBuilderExceptionItem = { ...getExceptionListItemSchemaMock(), entries: [ @@ -664,10 +664,10 @@ describe('Exception builder helpers', () => { }, ], }; - const output = getUpdatedEntriesOnDelete(payloadItem, 0, 1); + const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0); const expected: ExceptionsBuilderExceptionItem = { ...getExceptionListItemSchemaMock(), - entries: [{ ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] }], + entries: [{ ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }], }; expect(output).toEqual(expected); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 8585f58504e3..f6b703b7e622 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -248,22 +248,22 @@ export const getFormattedBuilderEntries = ( export const getUpdatedEntriesOnDelete = ( exceptionItem: ExceptionsBuilderExceptionItem, entryIndex: number, - nestedEntryIndex: number | null + nestedParentIndex: number | null ): ExceptionsBuilderExceptionItem => { - const itemOfInterest: BuilderEntry = exceptionItem.entries[entryIndex]; + const itemOfInterest: BuilderEntry = exceptionItem.entries[nestedParentIndex ?? entryIndex]; - if (nestedEntryIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) { + if (nestedParentIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) { const updatedEntryEntries: Array = [ - ...itemOfInterest.entries.slice(0, nestedEntryIndex), - ...itemOfInterest.entries.slice(nestedEntryIndex + 1), + ...itemOfInterest.entries.slice(0, entryIndex), + ...itemOfInterest.entries.slice(entryIndex + 1), ]; if (updatedEntryEntries.length === 0) { return { ...exceptionItem, entries: [ - ...exceptionItem.entries.slice(0, entryIndex), - ...exceptionItem.entries.slice(entryIndex + 1), + ...exceptionItem.entries.slice(0, nestedParentIndex), + ...exceptionItem.entries.slice(nestedParentIndex + 1), ], }; } else { @@ -277,9 +277,9 @@ export const getUpdatedEntriesOnDelete = ( return { ...exceptionItem, entries: [ - ...exceptionItem.entries.slice(0, entryIndex), + ...exceptionItem.entries.slice(0, nestedParentIndex), updatedItemOfInterest, - ...exceptionItem.entries.slice(entryIndex + 1), + ...exceptionItem.entries.slice(nestedParentIndex + 1), ], }; } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx new file mode 100644 index 000000000000..3fa0e59f9acb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx @@ -0,0 +1,422 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { wait as waitFor } from '@testing-library/react'; + +import { + fields, + getField, +} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { getEmptyValue } from '../../empty_value'; + +import { ExceptionBuilderComponent } from './'; + +jest.mock('../../../../common/lib/kibana'); + +describe('ExceptionBuilderComponent', () => { + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + getValueSuggestionsMock.mockClear(); + }); + + test('it displays empty entry if no "exceptionListItems" are passed in', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('Search'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + 'Search field value...' + ); + }); + + test('it displays "exceptionListItems" that are passed in', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is one of' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual( + 'some ip' + ); + + wrapper.unmount(); + }); + + test('it displays "or", "and" and "add nested button" enabled', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionsAndButton"] button').prop('disabled') + ).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionsOrButton"] button').prop('disabled') + ).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionsNestedButton"] button').prop('disabled') + ).toBeFalsy(); + }); + + test('it adds an entry when "and" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + + wrapper.find('[data-test-subj="exceptionsAndButton"] button').simulate('click'); + + await waitFor(() => { + expect( + wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]') + ).toHaveLength(2); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text() + ).toEqual('Search field value...'); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').at(1).text()).toEqual( + 'Search' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(1).text()).toEqual( + 'is' + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(1).text() + ).toEqual('Search field value...'); + }); + }); + + test('it adds an exception item when "or" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]')).toHaveLength( + 1 + ); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + await waitFor(() => { + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]')).toHaveLength( + 2 + ); + + const item1 = wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]').at(0); + const item2 = wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]').at(1); + + expect(item1.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(item1.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect(item1.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text()).toEqual( + 'Search field value...' + ); + + expect(item2.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(item2.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect(item2.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text()).toEqual( + 'Search field value...' + ); + }); + }); + + test('it displays empty entry if user deletes last remaining entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is one of' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual( + 'some ip' + ); + + wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"] button').simulate('click'); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('Search'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + 'Search field value...' + ); + + wrapper.unmount(); + }); + + test('it displays "and" badge if at least one exception item includes more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + + wrapper.find('[data-test-subj="exceptionsAndButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it does not display "and" badge if none of the exception items include more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + }); + + describe('nested entry', () => { + test('it adds a nested entry when "add nested entry" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsNestedButton"] button').simulate('click'); + + await waitFor(() => { + const entry2 = wrapper + .find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]') + .at(1); + expect(entry2.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search nested field' + ); + expect( + entry2.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text() + ).toEqual('is'); + expect( + entry2.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').at(0).text() + ).toEqual(getEmptyValue()); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index b82607a541aa..165f3314c2f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import { ExceptionListItemComponent } from './builder_exception_item'; +import { BuilderExceptionListItemComponent } from './exception_item'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { ExceptionListItemSchema, @@ -20,7 +20,7 @@ import { entriesNested, } from '../../../../../public/lists_plugin_deps'; import { AndOrBadge } from '../../and_or_badge'; -import { BuilderButtonOptions } from './builder_button_options'; +import { BuilderLogicButtons } from './logic_buttons'; import { getNewExceptionItem, filterExceptionItems } from '../helpers'; import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; import { State, exceptionsBuilderReducer } from './reducer'; @@ -72,7 +72,7 @@ interface ExceptionBuilderProps { onChange: (arg: OnChangeProps) => void; } -export const ExceptionBuilder = ({ +export const ExceptionBuilderComponent = ({ exceptionListItems, listType, listId, @@ -310,6 +310,8 @@ export const ExceptionBuilder = ({ onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete }); }, [onChange, exceptionsToDelete, exceptions]); + // Defaults builder to never be sans entry, instead + // always falls back to an empty entry if user deletes all useEffect(() => { if ( exceptions.length === 0 || @@ -351,13 +353,12 @@ export const ExceptionBuilder = ({ ))} - )} - ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} )); -storiesOf('Components|Exceptions|BuilderButtonOptions', module) +storiesOf('Exceptions|BuilderLogicButtons', module) .add('and/or buttons', () => { return ( - { return ( - { return ( - { return ( - { return ( - { return ( - { +describe('BuilderLogicButtons', () => { test('it renders "and" and "or" buttons', () => { const wrapper = mount( - { const onOrClicked = jest.fn(); const wrapper = mount( - { const onAndClicked = jest.fn(); const wrapper = mount( - { const onAddClickWhenNested = jest.fn(); const wrapper = mount( - { test('it disables "and" button if "isAndDisabled" is true', () => { const wrapper = mount( - { test('it disables "or" button if "isOrDisabled" is "true"', () => { const wrapper = mount( - { test('it disables "add nested" button if "isNestedDisabled" is "true"', () => { const wrapper = mount( - { const onNestedClicked = jest.fn(); const wrapper = mount( - { const onAndClicked = jest.fn(); const wrapper = mount( - void; } -export const BuilderButtonOptions: React.FC = ({ +export const BuilderLogicButtons: React.FC = ({ isOrDisabled = false, isAndDisabled = false, showNestedButton = false, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts new file mode 100644 index 000000000000..ee5bd1329f35 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts @@ -0,0 +1,441 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; +import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock'; +import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock'; + +import { ExceptionsBuilderExceptionItem } from '../types'; +import { Action, State, exceptionsBuilderReducer } from './reducer'; +import { getDefaultEmptyEntry } from './helpers'; + +const initialState: State = { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: false, + addNested: false, + exceptions: [], + exceptionsToDelete: [], +}; + +describe('exceptionsBuilderReducer', () => { + let reducer: (state: State, action: Action) => State; + + beforeEach(() => { + reducer = exceptionsBuilderReducer(); + }); + + describe('#setExceptions', () => { + test('should return "andLogicIncluded" ', () => { + const update = reducer(initialState, { + type: 'setExceptions', + exceptions: [], + }); + + expect(update).toEqual({ + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: false, + addNested: false, + exceptions: [], + exceptionsToDelete: [], + }); + }); + + test('should set "andLogicIncluded" to true if any of the exceptions include entries with length greater than 1 ', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryMatchMock()], + }, + ]; + const { andLogicIncluded } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(andLogicIncluded).toBeTruthy(); + }); + + test('should set "andLogicIncluded" to false if any of the exceptions include entries with length greater than 1 ', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { andLogicIncluded } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(andLogicIncluded).toBeFalsy(); + }); + + test('should set "addNested" to true if last exception entry is type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { addNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(addNested).toBeTruthy(); + }); + + test('should set "addNested" to false if last exception item entry is not type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { addNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(addNested).toBeFalsy(); + }); + + test('should set "disableOr" to true if last exception entry is type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { disableOr } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableOr).toBeTruthy(); + }); + + test('should set "disableOr" to false if last exception item entry is not type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableOr } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableOr).toBeFalsy(); + }); + + test('should set "disableNested" to true if an exception item includes an entry of type list', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { disableNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableNested).toBeTruthy(); + }); + + test('should set "disableNested" to false if an exception item does not include an entry of type list', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableNested).toBeFalsy(); + }); + + // What does that even mean?! :) Just checking if a user has selected + // to add a nested entry but has not yet selected the nested field + test('should set "disableAnd" to true if last exception item is a nested entry with no entries itself', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], + }, + ]; + const { disableAnd } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableAnd).toBeTruthy(); + }); + + test('should set "disableAnd" to false if last exception item is a nested entry with no entries itself', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableAnd } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableAnd).toBeFalsy(); + }); + }); + + describe('#setDefault', () => { + test('should restore initial state and add default empty entry to item" ', () => { + const update = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDefault', + initialState, + lastException: { + ...getExceptionListItemSchemaMock(), + entries: [], + }, + } + ); + + expect(update).toEqual({ + ...initialState, + exceptions: [ + { + ...getExceptionListItemSchemaMock(), + entries: [getDefaultEmptyEntry()], + }, + ], + }); + }); + }); + + describe('#setExceptionsToDelete', () => { + test('should add passed in exception item to "exceptionsToDelete"', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + id: '2', + entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], + }, + ]; + const { exceptionsToDelete } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions, + exceptionsToDelete: [], + }, + { + type: 'setExceptionsToDelete', + exceptions: [ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + ], + } + ); + + expect(exceptionsToDelete).toEqual([ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + ]); + }); + }); + + describe('#setDisableAnd', () => { + test('should set "disableAnd" to false if "action.shouldDisable" is false', () => { + const { disableAnd } = reducer( + { + disableAnd: true, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableAnd', + shouldDisable: false, + } + ); + + expect(disableAnd).toBeFalsy(); + }); + + test('should set "disableAnd" to true if "action.shouldDisable" is true', () => { + const { disableAnd } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableAnd', + shouldDisable: true, + } + ); + + expect(disableAnd).toBeTruthy(); + }); + }); + + describe('#setDisableOr', () => { + test('should set "disableOr" to false if "action.shouldDisable" is false', () => { + const { disableOr } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: true, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableOr', + shouldDisable: false, + } + ); + + expect(disableOr).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.shouldDisable" is true', () => { + const { disableOr } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableOr', + shouldDisable: true, + } + ); + + expect(disableOr).toBeTruthy(); + }); + }); + + describe('#setAddNested', () => { + test('should set "addNested" to false if "action.addNested" is false', () => { + const { addNested } = reducer( + { + disableAnd: false, + disableNested: true, + disableOr: false, + andLogicIncluded: true, + addNested: true, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setAddNested', + addNested: false, + } + ); + + expect(addNested).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.addNested" is true', () => { + const { addNested } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setAddNested', + addNested: true, + } + ); + + expect(addNested).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 6109b85f2da5..e1352ac38dc4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -31,7 +31,7 @@ import { import * as i18n from './translations'; import { useKibana } from '../../../lib/kibana'; import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { ExceptionBuilder } from '../builder'; +import { ExceptionBuilderComponent } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -232,7 +232,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {i18n.EXCEPTION_BUILDER_INFO} - } > - - + + +

+ Additional filters here. +

+
+
+ `; diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx index 9fda60b1f289..d9032092744a 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx @@ -6,7 +6,6 @@ import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; -import { StickyContainer } from 'react-sticky'; import '../../mock/match_media'; import { FiltersGlobal } from './filters_global'; @@ -15,7 +14,7 @@ import { TestProviders } from '../../mock/test_providers'; describe('rendering', () => { test('renders correctly', () => { const wrapper = shallow( - +

{'Additional filters here.'}

); @@ -23,101 +22,40 @@ describe('rendering', () => { expect(wrapper).toMatchSnapshot(); }); - describe('full screen mode', () => { + describe('when show is true (the default)', () => { let wrapper: ReactWrapper; beforeEach(() => { wrapper = mount( - - -

{'Filter content'}

-
-
+ +

{'Filter content'}

+
); }); - test('it does NOT render the sticky container', () => { - expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( - false - ); - }); - - test('it renders the non-sticky container', () => { - expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(true); - }); - test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { expect( - wrapper.find('[data-test-subj="non-sticky-global-container"]').first() - ).not.toHaveStyleRule('display', 'none'); - }); - }); - - describe('non-full screen mode', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - - - -

{'Filter content'}

-
-
-
- ); - }); - - test('it renders the sticky container', () => { - expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( - true - ); - }); - - test('it does NOT render the non-sticky container', () => { - expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(false); - }); - - test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { - expect( - wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() + wrapper.find('[data-test-subj="filters-global-container"]').first() ).not.toHaveStyleRule('display', 'none'); }); }); describe('when show is false', () => { - test('in full screen mode it renders the container with a `display: none` style', () => { + test('it renders the container with a `display: none` style', () => { const wrapper = mount( - - -

{'Filter content'}

-
-
+ +

{'Filter content'}

+
); - expect( - wrapper.find('[data-test-subj="non-sticky-global-container"]').first() - ).toHaveStyleRule('display', 'none'); - }); - - test('in non-full screen mode it renders the container with a `display: none` style', () => { - const wrapper = mount( - - - -

{'Filter content'}

-
-
-
+ expect(wrapper.find('[data-test-subj="filters-global-container"]').first()).toHaveStyleRule( + 'display', + 'none' ); - - expect( - wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() - ).toHaveStyleRule('display', 'none'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index 80e7209492fa..c324b812a9ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -4,38 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; -import { Sticky } from 'react-sticky'; import styled, { css } from 'styled-components'; +import { InPortal } from 'react-reverse-portal'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { gutterTimeline } from '../../lib/helpers'; -const offsetChrome = 49; - -const disableSticky = `screen and (max-width: ${euiLightVars.euiBreakpoints.s})`; -const disableStickyMq = window.matchMedia(disableSticky); - -const Wrapper = styled.aside<{ isSticky?: boolean }>` - height: ${FILTERS_GLOBAL_HEIGHT}px; +const Wrapper = styled.aside` position: relative; z-index: ${({ theme }) => theme.eui.euiZNavigation}; background: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} ${({ theme }) => - theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; - - ${({ isSticky }) => - isSticky && - css` - top: ${offsetChrome}px !important; - `} - - @media only ${disableSticky} { - position: static !important; - z-index: ${({ theme }) => theme.eui.euiZContent} !important; - } + padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} + ${({ theme }) => theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; `; Wrapper.displayName = 'Wrapper'; @@ -47,33 +29,21 @@ const FiltersGlobalContainer = styled.header<{ show: boolean }>` FiltersGlobalContainer.displayName = 'FiltersGlobalContainer'; -const NO_STYLE: React.CSSProperties = {}; - export interface FiltersGlobalProps { children: React.ReactNode; - globalFullScreen: boolean; show?: boolean; } -export const FiltersGlobal = React.memo( - ({ children, globalFullScreen, show = true }) => - globalFullScreen ? ( - - - {children} - +export const FiltersGlobal = React.memo(({ children, show = true }) => { + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); + + return ( + + + {children} - ) : ( - - {({ style, isSticky }) => ( - - - {children} - - - )} - - ) -); + + ); +}); FiltersGlobal.displayName = 'FiltersGlobal'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index a1e7293ce974..fbc3d62768d0 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -7,29 +7,31 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { pickBy } from 'lodash/fp'; import React, { useCallback } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; +import { OutPortal } from 'react-reverse-portal'; import { gutterTimeline } from '../../lib/helpers'; import { navTabs } from '../../../app/home/home_navigations'; +import { useFullScreen } from '../../containers/use_full_screen'; import { SecurityPageName } from '../../../app/types'; import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; import { useWithSource } from '../../containers/source'; -import { useFullScreen } from '../../containers/use_full_screen'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; +import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { LinkAnchor } from '../links'; -const Wrapper = styled.header<{ show: boolean }>` - ${({ show, theme }) => css` +const Wrapper = styled.header<{ $globalFullScreen: boolean }>` + ${({ $globalFullScreen, theme }) => ` background: ${theme.eui.euiColorEmptyShade}; border-bottom: ${theme.eui.euiBorderThin}; - padding: ${theme.eui.paddingSizes.m} ${gutterTimeline} ${theme.eui.paddingSizes.m} - ${theme.eui.paddingSizes.l}; - ${show ? '' : 'display: none;'}; + padding-top: ${$globalFullScreen ? theme.eui.paddingSizes.s : theme.eui.paddingSizes.m}; + width: 100%; + z-index: ${theme.eui.euiZNavigation}; `} `; Wrapper.displayName = 'Wrapper'; @@ -39,11 +41,24 @@ const FlexItem = styled(EuiFlexItem)` `; FlexItem.displayName = 'FlexItem'; +const FlexGroup = styled(EuiFlexGroup)<{ $globalFullScreen: boolean }>` + ${({ $globalFullScreen, theme }) => ` + border-bottom: ${theme.eui.euiBorderThin}; + margin-bottom: 1px; + padding-bottom: 4px; + padding-left: ${theme.eui.paddingSizes.l}; + padding-right: ${gutterTimeline}; + ${$globalFullScreen ? 'display: none;' : ''} + `} +`; +FlexGroup.displayName = 'FlexGroup'; + interface HeaderGlobalProps { hideDetectionEngine?: boolean; } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { const { indicesExist } = useWithSource(); + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); const { globalFullScreen } = useFullScreen(); const search = useGetUrlSearch(navTabs.overview); const { navigateToApp } = useKibana().services.application; @@ -56,8 +71,13 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine ); return ( - - + + <> @@ -100,7 +120,10 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - + +
+ +
); }); diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx index d5d670b4c03f..038c116c9fc8 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx @@ -8,7 +8,6 @@ import { EuiButton, EuiButtonEmpty, EuiCheckbox, - // @ts-ignore no-exported-member EuiFilePicker, EuiModal, EuiModalBody, diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 3dc120b3d874..435f3f6e349d 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -20,6 +20,7 @@ import * as i18n from './translations'; export const BUTTON_CLASS = 'inspectButtonComponent'; export const InspectButtonContainer = styled.div<{ show?: boolean }>` + width: 100%; display: flex; flex-grow: 1; diff --git a/x-pack/plugins/security_solution/public/common/components/loader/index.tsx b/x-pack/plugins/security_solution/public/common/components/loader/index.tsx index e78f14841858..cd660ae61161 100644 --- a/x-pack/plugins/security_solution/public/common/components/loader/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/loader/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - // @ts-ignore + // @ts-expect-error EuiLoadingSpinnerSize, EuiText, } from '@elastic/eui'; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index fa512ad1ed80..e93ade7191f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -64,7 +64,6 @@ const HeaderChildrenFlexItem = styled(EuiFlexItem)` margin-left: 24px; `; -// @ts-ignore - the EUI type definitions for Panel do no play nice with styled-components const HistogramPanel = styled(Panel)<{ height?: number }>` display: flex; flex-direction: column; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 78cd23e6647c..9bfae686b1a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -77,9 +77,9 @@ const AnomaliesHostTableComponent: React.FC = ({ /> type is not as specific as EUI's... + // @ts-expect-error the Columns type is not as specific as EUI's... columns={columns} - // @ts-ignore ...which leads to `networks` not "matching" the columns + // @ts-expect-error ...which leads to `networks` not "matching" the columns items={hosts} pagination={pagination} sorting={sorting} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index 73fe7b1ea5f6..af27d411b990 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -67,9 +67,9 @@ const AnomaliesNetworkTableComponent: React.FC = ({ /> type is not as specific as EUI's... + // @ts-expect-error the Columns type is not as specific as EUI's... columns={columns} - // @ts-ignore ...which leads to `networks` not "matching" the columns + // @ts-expect-error ...which leads to `networks` not "matching" the columns items={networks} pagination={pagination} sorting={sorting} diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index 8cb35fc68918..4cfb7f8ad2b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -11,7 +11,6 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, - // @ts-ignore no-exported-member EuiSearchBar, } from '@elastic/eui'; import { EuiSearchBarQuery } from '../../../../../timelines/components/open_timeline/types'; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 8bf0690bfd0a..140429dc4abd 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -62,15 +62,40 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar z-index: 9950; } - /** applies a "toggled" button style to the Full Screen button */ + /* applies a "toggled" button style to the Full Screen button */ .${FULL_SCREEN_TOGGLED_CLASS_NAME} { ${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`}; } - .${SCROLLING_DISABLED_CLASS_NAME} body { + body { overflow-y: hidden; } + #kibana-body { + height: 100%; + overflow-y: hidden; + + > .content { + height: 100%; + + > .app-wrapper { + height: 100%; + + > .app-wrapper-panel { + height: 100%; + + > .application { + height: 100%; + + > div { + height: 100%; + } + } + } + } + } + } + .${SCROLLING_DISABLED_CLASS_NAME} #kibana-body { overflow-y: hidden; } diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index 03f9b4367800..373c1f7aaec7 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -12,12 +12,9 @@ import { useFullScreen } from '../../containers/use_full_screen'; import { gutterTimeline } from '../../lib/helpers'; import { AppGlobalStyle } from '../page/index'; -const Wrapper = styled.div<{ noPadding?: boolean }>` - padding: ${(props) => - props.noPadding - ? '0' - : `${props.theme.eui.paddingSizes.l} ${gutterTimeline} ${props.theme.eui.paddingSizes.l} - ${props.theme.eui.paddingSizes.l}`}; +const Wrapper = styled.div` + padding: ${({ theme }) => + `${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l}`}; &.siemWrapperPage--restrictWidthDefault, &.siemWrapperPage--restrictWidthCustom { box-sizing: content-box; @@ -27,6 +24,14 @@ const Wrapper = styled.div<{ noPadding?: boolean }>` &.siemWrapperPage--restrictWidthDefault { max-width: 1000px; } + + &.siemWrapperPage--fullHeight { + height: 100%; + } + + &.siemWrapperPage--noPadding { + padding: 0; + } `; Wrapper.displayName = 'Wrapper'; @@ -46,13 +51,15 @@ const WrapperPageComponent: React.FC = ({ style, noPadding, }) => { - const { setGlobalFullScreen } = useFullScreen(); + const { globalFullScreen, setGlobalFullScreen } = useFullScreen(); useEffect(() => { setGlobalFullScreen(false); // exit full screen mode on page load }, [setGlobalFullScreen]); const classes = classNames(className, { siemWrapperPage: true, + 'siemWrapperPage--noPadding': noPadding, + 'siemWrapperPage--fullHeight': globalFullScreen, 'siemWrapperPage--restrictWidthDefault': restrictWidth && typeof restrictWidth === 'boolean' && restrictWidth === true, 'siemWrapperPage--restrictWidthCustom': restrictWidth && typeof restrictWidth !== 'boolean', @@ -66,7 +73,7 @@ const WrapperPageComponent: React.FC = ({ } return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx index e1b192df104d..1bcbebd12b9b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx @@ -14,8 +14,7 @@ import { onError } from 'apollo-link-error'; const mockDispatch = jest.fn(); jest.mock('apollo-link-error'); jest.mock('../../store'); -// @ts-ignore -store.getStore.mockReturnValue({ dispatch: mockDispatch }); +(store.getStore as jest.Mock).mockReturnValue({ dispatch: mockDispatch }); describe('errorLinkHandler', () => { const mockGraphQLErrors: GraphQLError = { diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx index aa0d90a21603..32591fb03243 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -11,6 +11,22 @@ import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants'; import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; +export const resetScroll = () => { + setTimeout(() => { + window.scrollTo(0, 0); + + const kibanaBody = document.querySelector('#kibana-body'); + if (kibanaBody != null) { + kibanaBody.scrollTop = 0; + } + + const pageContainer = document.querySelector('[data-test-subj="pageContainer"]'); + if (pageContainer != null) { + pageContainer.scrollTop = 0; + } + }, 0); +}; + export const useFullScreen = () => { const dispatch = useDispatch(); const globalFullScreen = useSelector(inputsSelectors.globalFullScreenSelector) ?? false; @@ -20,9 +36,10 @@ export const useFullScreen = () => { (fullScreen: boolean) => { if (fullScreen) { document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME); + resetScroll(); } else { document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME); - setTimeout(() => window.scrollTo(0, 0), 0); + resetScroll(); } dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen })); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx new file mode 100644 index 000000000000..546d2615fe6a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx @@ -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. + */ + +import { useState } from 'react'; +import { createPortalNode } from 'react-reverse-portal'; + +/** + * A singleton portal for rendering content in the global header + */ +const globalHeaderPortalNodeSingleton = createPortalNode(); + +export const useGlobalHeaderPortal = () => { + const [globalHeaderPortalNode] = useState(globalHeaderPortalNodeSingleton); + + return { globalHeaderPortalNode }; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts b/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts index 4a3d734d0a6d..c34027648c89 100644 --- a/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts @@ -18,10 +18,8 @@ jest.mock('../../containers/errors'); const mockWithClientState = 'mockWithClientState'; const mockHttpLink = { mockHttpLink: 'mockHttpLink' }; -// @ts-ignore -withClientState.mockReturnValue(mockWithClientState); -// @ts-ignore -apolloLinkHttp.createHttpLink.mockImplementation(() => mockHttpLink); +(withClientState as jest.Mock).mockReturnValue(mockWithClientState); +(apolloLinkHttp.createHttpLink as jest.Mock).mockImplementation(() => mockHttpLink); describe('getLinks helper', () => { test('It should return links in correct order', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 16d1a1481bc9..c2b51e29c230 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -255,8 +255,7 @@ describe('alert actions', () => { nonEcsData: [], updateTimelineIsLoading, }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; + const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); @@ -285,8 +284,7 @@ describe('alert actions', () => { nonEcsData: [], updateTimelineIsLoading, }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; + const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index f38a9107afca..be9725dac5ff 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -95,7 +95,7 @@ export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): key: 'signal.rule.building_block_type', value: 'exists', }, - // @ts-ignore TODO: Rework parent typings to support ExistsFilter[] + // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] exists: { field: 'signal.rule.building_block_type' }, }, ]), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index ab95e433d92f..d93bad29f334 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -61,7 +61,6 @@ interface OwnProps { timelineId: TimelineIdLiteral; canUserCRUD: boolean; defaultFilters?: Filter[]; - eventsViewerBodyHeight?: number; hasIndexWrite: boolean; from: string; loading: boolean; @@ -88,7 +87,6 @@ export const AlertsTableComponent: React.FC = ({ clearEventsLoading, clearSelected, defaultFilters, - eventsViewerBodyHeight, from, globalFilters, globalQuery, @@ -451,7 +449,6 @@ export const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={to} headerFilterGroup={headerFilterGroup} - height={eventsViewerBodyHeight} id={timelineId} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index e5e8635b9e79..3d6c3dc0a7a8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -118,7 +118,7 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( export const ACTION_ADD_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addException', { - defaultMessage: 'Add exception', + defaultMessage: 'Add rule exception', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 0b341050fa9d..47c12d193417 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -213,6 +213,8 @@ export const getDescriptionItem = ( } else if (field === 'ruleType') { const ruleType: RuleType = get(field, data); return buildRuleTypeDescription(label, ruleType); + } else if (field === 'kibanaSiemAppUrl') { + return []; } const description: string = get(field, data); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index 20470d7bb924..a3db8fe659d8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -96,7 +96,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel', { - defaultMessage: 'Associate rule to Global Endpoint Exception List', + defaultMessage: 'Add existing Endpoint exceptions to the rule', } ), labelAppend: OptionalFieldLabel, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index 939747717385..f4d90d0596ed 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -30,7 +30,7 @@ export const ADD_FALSE_POSITIVE = i18n.translate( export const GLOBAL_ENDPOINT_EXCEPTION_LIST = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel', { - defaultMessage: 'Global endpoint exception list', + defaultMessage: 'Elastic Endpoint exceptions', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts index 7063dca2341c..9beebdfb923d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts @@ -13,7 +13,7 @@ export const MODAL_TITLE = i18n.translate('xpack.securitySolution.lists.uploadVa export const FILE_PICKER_LABEL = i18n.translate( 'xpack.securitySolution.lists.uploadValueListDescription', { - defaultMessage: 'Upload single value lists to use while writing rules or rule exceptions.', + defaultMessage: 'Upload single value lists to use while writing rule exceptions.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index c114e4519df1..d76da592e1c8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -7,12 +7,9 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; -import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; -import { useWindowSize } from 'react-use'; import { useHistory } from 'react-router-dom'; -import { globalHeaderHeightPx } from '../../../app/home'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { useGlobalTime } from '../../../common/containers/use_global_time'; @@ -34,7 +31,6 @@ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; import { useUserInfo } from '../../components/user_info'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../common/components/events_viewer/events_viewer'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; @@ -43,14 +39,8 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import { Display } from '../../../hosts/pages/display'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -64,7 +54,6 @@ export const DetectionEnginePageComponent: React.FC = ({ setAbsoluteRangeDatePicker, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); const { loading: userInfoLoading, @@ -157,12 +146,9 @@ export const DetectionEnginePageComponent: React.FC = ({ {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } {indicesExist ? ( - + <> - + @@ -210,17 +196,6 @@ export const DetectionEnginePageComponent: React.FC = ({ loading={loading} hasIndexWrite={hasIndexWrite ?? false} canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)} - eventsViewerBodyHeight={ - globalFullScreen - ? getEventsViewerBodyHeight({ - footerHeight, - headerHeight: EVENTS_VIEWER_HEADER_HEIGHT, - kibanaChromeHeight: globalHeaderHeightPx, - otherContentHeight: FILTERS_GLOBAL_HEIGHT, - windowHeight, - }) - : MIN_EVENTS_VIEWER_BODY_HEIGHT - } from={from} defaultFilters={alertsTableDefaultFilters} showBuildingBlockAlerts={showBuildingBlockAlerts} @@ -229,7 +204,7 @@ export const DetectionEnginePageComponent: React.FC = ({ to={to} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 789469e981fb..4327ef96c93a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -21,11 +21,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; -import { useWindowSize } from 'react-use'; -import { globalHeaderHeightPx } from '../../../../../app/home'; import { TimelineId } from '../../../../../../common/types/timeline'; import { UpdateDateRange } from '../../../../../common/components/charts/common'; import { FiltersGlobal } from '../../../../../common/components/filters_global'; @@ -66,7 +63,6 @@ import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../../../common/components/events_viewer/events_viewer'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { State } from '../../../../../common/store'; import { InputsRange } from '../../../../../common/store/inputs/model'; @@ -81,15 +77,10 @@ import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; -import { DEFAULT_INDEX_PATTERN, FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../shared_imports'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../../../timelines/components/timeline/footer'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; @@ -167,7 +158,6 @@ export const RuleDetailsPageComponent: FC = ({ const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check @@ -364,12 +354,9 @@ export const RuleDetailsPageComponent: FC = ({ {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions(canUserCRUD) && } {indicesExist ? ( - + <> - + @@ -507,17 +494,6 @@ export const RuleDetailsPageComponent: FC = ({ timelineId={TimelineId.detectionsRulesDetailsPage} canUserCRUD={canUserCRUD ?? false} defaultFilters={alertDefaultFilters} - eventsViewerBodyHeight={ - globalFullScreen - ? getEventsViewerBodyHeight({ - footerHeight, - headerHeight: EVENTS_VIEWER_HEADER_HEIGHT, - kibanaChromeHeight: globalHeaderHeightPx, - otherContentHeight: FILTERS_GLOBAL_HEIGHT, - windowHeight, - }) - : MIN_EVENTS_VIEWER_BODY_HEIGHT - } hasIndexWrite={hasIndexWrite ?? false} from={from} loading={loading} @@ -542,7 +518,7 @@ export const RuleDetailsPageComponent: FC = ({ )} {ruleDetailTab === RuleDetailTabs.failures && } - + ) : ( diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 84096e242cbb..7b20873bf63c 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -229,7 +229,11 @@ { "name": "pageInfo", "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null } + }, "defaultValue": null }, { @@ -10905,13 +10909,21 @@ { "name": "pageIndex", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null }, { "name": "pageSize", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null } ], @@ -13142,24 +13154,6 @@ "interfaces": null, "enumValues": null, "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "TemplateTimelineType", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "elastic", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null } ], "directives": [ diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 90d1b8bd54df..f7d2c81f536b 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -102,9 +102,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -423,11 +423,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2324,7 +2319,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index b6c1727ee6af..34840b282662 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -8,7 +8,6 @@ import { EuiHorizontalRule, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { SecurityPageName } from '../../../app/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; @@ -102,12 +101,9 @@ const HostDetailsComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -210,7 +206,7 @@ const HostDetailsComponent = React.memo( setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 1d0b73f80a69..e4e69443c510 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -8,7 +8,6 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { useParams } from 'react-router-dom'; import { SecurityPageName } from '../../app/types'; @@ -96,12 +95,9 @@ export const HostsComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -156,7 +152,7 @@ export const HostsComponent = React.memo( hostsPagePath={hostsPagePath} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 962c227d6b67..cea987db485f 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -6,7 +6,6 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import { useWindowSize } from 'react-use'; import { TimelineId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; @@ -22,15 +21,7 @@ import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { - getEventsViewerBodyHeight, - getInvestigateInResolverAction, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; -import { globalHeaderHeightPx } from '../../../app/home'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../common/components/events_viewer/events_viewer'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; +import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -71,7 +62,6 @@ export const EventsQueryTabBody = ({ }: HostsComponentsQueryProps) => { const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); useEffect(() => { initializeTimeline({ @@ -108,17 +98,6 @@ export const EventsQueryTabBody = ({ { - // @ts-ignore + // @ts-expect-error apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index e7aa2c8893f8..43a6ad2c585b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -109,14 +109,14 @@ export const policyDetailsReducer: ImmutableReducer { /** * this is not safe because `action.payload.policyConfig` may have excess keys */ - // @ts-ignore + // @ts-expect-error newPolicy[section as keyof UIPolicyConfig] = { ...newPolicy[section as keyof UIPolicyConfig], ...newSettings, diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index c58e53d07acb..25928197590e 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -114,7 +114,7 @@ export const createEmbeddable = async ( if (!isErrorEmbeddable(embeddableObject)) { embeddableObject.setRenderTooltipContent(renderTooltipContent); - // @ts-ignore + // @ts-expect-error await embeddableObject.setLayerList(getLayerList(indexPatterns)); } diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index 42469a4bf29d..c9ac1fc7a6e9 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -7,7 +7,6 @@ import { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; import React, { useCallback, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { FiltersGlobal } from '../../../common/components/filters_global'; @@ -89,8 +88,8 @@ export const IPDetailsComponent: React.FC {indicesExist ? ( - - + <> + @@ -260,7 +259,7 @@ export const IPDetailsComponent: React.FC - + ) : ( diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index f516f2a2de34..601bae89f7a4 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -9,7 +9,6 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import { esQuery } from '../../../../../../src/plugins/data/public'; import { SecurityPageName } from '../../app/types'; @@ -104,12 +103,9 @@ const NetworkComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -180,7 +176,7 @@ const NetworkComponent = React.memo( )} - + ) : ( diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx new file mode 100644 index 000000000000..99902a31975d --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx @@ -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 React from 'react'; +import { mount } from 'enzyme'; + +import { useKibana } from '../../../../common/lib/kibana'; +import '../../../../common/mock/match_media'; +import { createUseKibanaMock, TestProviders } from '../../../../common/mock'; +import { NoCases } from '.'; + +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mock; + +let navigateToApp: jest.Mock; + +describe('RecentCases', () => { + beforeEach(() => { + jest.resetAllMocks(); + navigateToApp = jest.fn(); + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockReturnValue({ + ...kibanaMock, + services: { + application: { + navigateToApp, + getUrlForApp: jest.fn(), + }, + }, + }); + }); + + it('if no cases, you should be able to create a case by clicking on the link "start a new case"', () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().simulate('click'); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { + path: + "/create?timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx index 40969a6e1df4..875a678f3222 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx @@ -21,7 +21,7 @@ const NoCasesComponent = () => { const goToCreateCase = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: getCreateCaseUrl(search), }); }, @@ -30,6 +30,7 @@ const NoCasesComponent = () => { const newCaseLink = useMemo( () => ( {` ${i18n.START_A_NEW_CASE}`} diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 1b743c259555..520fd6c45970 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { Query, Filter } from 'src/plugins/data/public'; import styled from 'styled-components'; @@ -70,8 +69,8 @@ const OverviewComponent: React.FC = ({ return ( <> {indicesExist ? ( - - + <> + @@ -140,7 +139,7 @@ const OverviewComponent: React.FC = ({
- + ) : ( )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap index 4bf0033bcb43..46c9fbb52406 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap @@ -2,7 +2,6 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` ({ StatefulTimeline: () =>
, })); -const testFlyoutHeight = 980; const usersViewing = ['elastic']; describe('Flyout', () => { @@ -39,7 +38,7 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); @@ -48,7 +47,7 @@ describe('Flyout', () => { test('it renders the default flyout state as a button', () => { const wrapper = mount( - + ); @@ -69,7 +68,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -94,7 +93,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -117,7 +116,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -127,7 +126,7 @@ describe('Flyout', () => { test('it hides the data providers badge when the timeline does NOT have data providers', () => { const wrapper = mount( - + ); @@ -152,7 +151,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -167,7 +166,6 @@ describe('Flyout', () => { ` Visible.displayName = 'Visible'; interface OwnProps { - flyoutHeight: number; timelineId: string; usersViewing: string[]; } @@ -44,7 +43,7 @@ interface OwnProps { type Props = OwnProps & ProsFromRedux; export const FlyoutComponent = React.memo( - ({ dataProviders, flyoutHeight, show = true, showTimeline, timelineId, usersViewing, width }) => { + ({ dataProviders, show = true, showTimeline, timelineId, usersViewing, width }) => { const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ showTimeline, timelineId, @@ -57,12 +56,7 @@ export const FlyoutComponent = React.memo( return ( <> - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap index d30fd6f31012..f24ef3448d03 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -2,7 +2,6 @@ exports[`Pane renders correctly against snapshot 1`] = ` { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( - + {'I am a child of flyout'} @@ -33,12 +27,7 @@ describe('Pane', () => { test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { const wrapper = mount( - + {'I am a child of flyout'} @@ -50,12 +39,7 @@ describe('Pane', () => { test('it should render a resize handle', () => { const wrapper = mount( - + {'I am a child of flyout'} @@ -67,12 +51,7 @@ describe('Pane', () => { test('it should render children', () => { const wrapper = mount( - + {'I am a mock body'} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 3f842bcc2eb6..7528468ef652 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -22,7 +22,6 @@ const minWidthPixels = 550; // do not allow the flyout to shrink below this widt const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view interface FlyoutPaneComponentProps { children: React.ReactNode; - flyoutHeight: number; onClose: () => void; timelineId: string; width: number; diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 97d1d11395c7..ededf7015296 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -34,8 +34,8 @@ import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal' import * as i18n from './translations'; -const OverlayContainer = styled.div<{ bodyHeight?: number }>` - height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; +const OverlayContainer = styled.div` + height: 100%; width: 100%; display: flex; flex-direction: column; @@ -50,7 +50,6 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` `; interface OwnProps { - bodyHeight?: number; graphEventId?: string; timelineId: string; timelineType: TimelineType; @@ -97,7 +96,6 @@ const Navigation = ({ ); const GraphOverlayComponent = ({ - bodyHeight, graphEventId, status, timelineId, @@ -140,7 +138,7 @@ const GraphOverlayComponent = ({ ]); return ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx index 957b37a0bd1c..7d083735e6c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx @@ -9,7 +9,6 @@ import { EuiInMemoryTableProps, EuiModalBody, EuiModalHeader, - EuiPanel, EuiSpacer, } from '@elastic/eui'; import React, { useState } from 'react'; @@ -20,7 +19,6 @@ import { Note } from '../../../common/lib/note'; import { AddNote } from './add_note'; import { columns } from './columns'; import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers'; -import { NOTES_PANEL_WIDTH, NOTES_PANEL_HEIGHT } from '../timeline/properties/notes_size'; import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline'; interface Props { @@ -32,23 +30,12 @@ interface Props { updateNote: UpdateNote; } -const NotesPanel = styled(EuiPanel)` - height: ${NOTES_PANEL_HEIGHT}px; - width: ${NOTES_PANEL_WIDTH}px; - - & thead { - display: none; - } -`; - -NotesPanel.displayName = 'NotesPanel'; - const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( EuiInMemoryTable as React.ComponentType> )` - overflow-x: hidden; - overflow-y: auto; - height: 220px; + & thead { + display: none; + } ` as any; // eslint-disable-line @typescript-eslint/no-explicit-any InMemoryTable.displayName = 'InMemoryTable'; @@ -60,7 +47,7 @@ export const Notes = React.memo( const isImmutable = status === TimelineStatus.immutable; return ( - + <> @@ -84,7 +71,7 @@ export const Notes = React.memo( sorting={true} /> - + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index f4bd17005fed..ac6c61b33b35 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -967,6 +967,7 @@ describe('helpers', () => { expect(dispatchAddTimeline).toHaveBeenCalledWith({ id: 'timeline-1', + savedTimeline: true, timeline: mockTimelineModel, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index af289f94c9a0..5b5c2c9eeafc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -238,7 +238,6 @@ export const getTimelineStatus = ( return duplicate ? TimelineStatus.active : timeline.status; }; -// eslint-disable-next-line complexity export const defaultTimelineToTimelineModel = ( timeline: TimelineResult, duplicate: boolean, @@ -374,7 +373,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli ruleNote, }: UpdateTimeline): (() => void) => () => { dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); - dispatch(dispatchAddTimeline({ id, timeline })); + dispatch(dispatchAddTimeline({ id, timeline, savedTimeline: duplicate })); if ( timeline.kqlQuery != null && timeline.kqlQuery.filterQuery != null && diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 75b6413bf08f..facdc392ff7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -4,29 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; -import React from 'react'; - // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; +import { useHistory, useParams } from 'react-router-dom'; + import '../../../common/mock/match_media'; +import { SecurityPageName } from '../../../app/types'; +import { TimelineType } from '../../../../common/types/timeline'; + import { TestProviders, apolloClient } from '../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../common/mock/timeline_results'; +import { getTimelineTabsUrl } from '../../../common/components/link_to'; + import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; +import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; +import { useTimelineStatus } from './use_timeline_status'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; - import { StatefulOpenTimeline } from '.'; +import { TimelineTabsStyle } from './types'; +import { + useTimelineTypes, + UseTimelineTypesArgs, + UseTimelineTypesResult, +} from './use_timeline_types'; -import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; - -import { useParams } from 'react-router-dom'; -import { TimelineType } from '../../../../common/types/timeline'; +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); -jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/components/link_to'); + return { + ...originalModule, + useParams: jest.fn(), + useHistory: jest.fn(), + }; +}); jest.mock('./helpers', () => { const originalModule = jest.requireActual('./helpers'); @@ -41,24 +59,39 @@ jest.mock('../../containers/all', () => { return { ...originalModule, useGetAllTimeline: jest.fn(), - getAllTimeline: originalModule.getAllTimeline, }; }); -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/components/link_to', () => { + const originalModule = jest.requireActual('../../../common/components/link_to'); return { ...originalModule, - useParams: jest.fn(), - useHistory: jest.fn().mockReturnValue([]), + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }), + }; +}); + +jest.mock('./use_timeline_status', () => { + return { + useTimelineStatus: jest.fn(), }; }); describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; + let mockHistory: History[]; + const mockInstallPrepackagedTimelines = jest.fn(); + beforeEach(() => { - (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.default, + pageName: SecurityPageName.timelines, + }); + mockHistory = []; + (useHistory as jest.Mock).mockReturnValue(mockHistory); ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), timelines: getAllTimeline( @@ -69,6 +102,20 @@ describe('StatefulOpenTimeline', () => { totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, refetch: jest.fn(), }); + ((useTimelineStatus as unknown) as jest.Mock).mockReturnValue({ + timelineStatus: null, + templateTimelineType: null, + templateTimelineFilter:
, + installPrepackagedTimelines: mockInstallPrepackagedTimelines, + }); + mockInstallPrepackagedTimelines.mockClear(); + }); + + afterEach(() => { + (getTimelineTabsUrl as jest.Mock).mockClear(); + (useParams as jest.Mock).mockClear(); + (useHistory as jest.Mock).mockClear(); + mockHistory = []; }); test('it has the expected initial state', () => { @@ -101,6 +148,109 @@ describe('StatefulOpenTimeline', () => { }); }); + describe("Template timelines' tab", () => { + test("should land on correct timelines' tab with url timelines/default", () => { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.default); + }); + + test("should land on correct timelines' tab with url timelines/template", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.template, + pageName: SecurityPageName.timelines, + }); + + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.template); + }); + + test("should land on correct templates' tab after switching tab", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.template, + pageName: SecurityPageName.timelines, + }); + + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}-${TimelineType.template}"]`) + .first() + .simulate('click'); + act(() => { + expect(history.length).toBeGreaterThan(0); + }); + }); + + test("should selecting correct timelines' filter", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: 'mockTabName', + pageName: SecurityPageName.case, + }); + + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.default); + }); + + test('should not change url after switching filter', () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: 'mockTabName', + pageName: SecurityPageName.case, + }); + + const wrapper = mount( + + + + + + ); + wrapper + .find( + `[data-test-subj="open-timeline-modal-body-${TimelineTabsStyle.filter}-${TimelineType.template}"]` + ) + .first() + .simulate('click'); + act(() => { + expect(mockHistory.length).toEqual(0); + }); + }); + }); + describe('#onQueryChange', () => { test('it updates the query state with the expected trimmed value when the user enters a query', () => { const wrapper = mount( @@ -482,9 +632,13 @@ describe('StatefulOpenTimeline', () => { ); - expect(wrapper.find('[data-test-subj="open-timeline-modal-body-filters"]').exists()).toEqual( - true - ); + expect( + wrapper + .find( + `[data-test-subj="open-timeline-modal-body-${TimelineTabsStyle.filter}-${TimelineType.default}"]` + ) + .exists() + ).toEqual(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx index 3017f553d59d..5ce53607817e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -12,10 +12,11 @@ import { ThemeProvider } from 'styled-components'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; + import { TestProviderWithoutDragAndDrop } from '../../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; import { useGetAllTimeline, getAllTimeline } from '../../../containers/all'; - +import { useTimelineStatus } from '../use_timeline_status'; import { OpenTimelineModal } from '.'; jest.mock('../../../../common/lib/kibana'); @@ -39,8 +40,15 @@ jest.mock('../use_timeline_types', () => { }; }); +jest.mock('../use_timeline_status', () => { + return { + useTimelineStatus: jest.fn(), + }; +}); + describe('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockInstallPrepackagedTimelines = jest.fn(); beforeEach(() => { ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), @@ -52,6 +60,16 @@ describe('OpenTimelineModal', () => { totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, refetch: jest.fn(), }); + ((useTimelineStatus as unknown) as jest.Mock).mockReturnValue({ + timelineStatus: null, + templateTimelineType: null, + templateTimelineFilter:
, + installPrepackagedTimelines: mockInstallPrepackagedTimelines, + }); + }); + + afterEach(() => { + mockInstallPrepackagedTimelines.mockClear(); }); test('it renders the expected modal', async () => { @@ -76,4 +94,25 @@ describe('OpenTimelineModal', () => { { timeout: 10000 } ); }, 20000); + + test('it installs elastic prebuilt templates', async () => { + const wrapper = mount( + + + + + + + + ); + + await waitFor( + () => { + wrapper.update(); + + expect(mockInstallPrepackagedTimelines).toHaveBeenCalled(); + }, + { timeout: 10000 } + ); + }, 20000); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx index 5b927db3c37a..69f79fb7aece 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx @@ -9,7 +9,6 @@ import { EuiFilterButton, EuiFlexGroup, EuiFlexItem, - // @ts-ignore EuiSearchBar, } from '@elastic/eui'; import React, { useMemo } from 'react'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx index 8c4c686698c8..37bea3d713c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx @@ -102,7 +102,7 @@ export const useTimelineStatus = ({ }, [templateTimelineType, filters, isTemplateFilterEnabled, onFilterClicked]); const installPrepackagedTimelines = useCallback(async () => { - if (templateTimelineType === TemplateTimelineType.elastic) { + if (templateTimelineType !== TemplateTimelineType.custom) { await installPrepackedTimelines(); } }, [templateTimelineType]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 55afe845cdfb..ec02124ae43d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -33,7 +33,9 @@ export const useTimelineTypes = ({ const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [timelineType, setTimelineTypes] = useState( - tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null + tabName === TimelineType.default || tabName === TimelineType.template + ? tabName + : TimelineType.default ); const goToTimeline = useCallback( @@ -98,7 +100,7 @@ export const useTimelineTypes = ({ (tabId, tabStyle: TimelineTabsStyle) => { setTimelineTypes((prevTimelineTypes) => { if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) { - return null; + return tabId === TimelineType.default ? TimelineType.template : TimelineType.default; } else if (prevTimelineTypes !== tabId) { setTimelineTypes(tabId); } @@ -114,6 +116,7 @@ export const useTimelineTypes = ({ {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( { return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( () => { const newSelection = xor([item], notExcludedRowRenderers); - // @ts-ignore + // @ts-expect-error ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions }, [notExcludedRowRenderers, ref] diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 73b5a58ef7b6..b62888fbf842 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -135,38 +135,3 @@ export const getInvestigateInResolverAction = ({ dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })), width: DEFAULT_ICON_BUTTON_WIDTH, }); - -/** - * The minimum height of a timeline-based events viewer body, as seen in several - * views, e.g. `Detections`, `Events`, `External events`, etc - */ -export const MIN_EVENTS_VIEWER_BODY_HEIGHT = 500; // px - -interface GetEventsViewerBodyHeightParams { - /** the height of the header, e.g. the section containing "`Showing n event / alerts`, and `Open` / `In progress` / `Closed` filters" */ - headerHeight: number; - /** the height of the footer, e.g. "`25 of 100 events / alerts`, `Load More`, `Updated n minutes ago`" */ - footerHeight: number; - /** the height of the global Kibana chrome, common throughout the app */ - kibanaChromeHeight: number; - /** the (combined) height of other non-events viewer content, e.g. the global search / filter bar in full screen mode */ - otherContentHeight: number; - /** the full height of the window */ - windowHeight: number; -} - -export const getEventsViewerBodyHeight = ({ - footerHeight, - headerHeight, - kibanaChromeHeight, - otherContentHeight, - windowHeight, -}: GetEventsViewerBodyHeightParams) => { - if (windowHeight === 0 || !isFinite(windowHeight)) { - return MIN_EVENTS_VIEWER_BODY_HEIGHT; - } - - const combinedHeights = kibanaChromeHeight + otherContentHeight + headerHeight + footerHeight; - - return Math.max(MIN_EVENTS_VIEWER_BODY_HEIGHT, windowHeight - combinedHeights); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 5a98263cbd3f..456c1ee54147 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -22,7 +22,6 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; import { TimelineType } from '../../../../../common/types/timeline'; -const testBodyHeight = 700; const mockGetNotesByIds = (eventId: string[]) => []; const mockSort: Sort = { columnId: '@timestamp', @@ -65,7 +64,6 @@ describe('Body', () => { data: mockTimelineData, docValueFields: [], eventIdToNoteIds: {}, - height: testBodyHeight, id: 'timeline-test', isSelectAllChecked: false, getNotesByIds: mockGetNotesByIds, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index e971dc6c8e1e..6f578ffe3e95 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -42,7 +42,6 @@ export interface BodyProps { docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; - height?: number; id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -85,7 +84,6 @@ export const Body = React.memo( eventIdToNoteIds, getNotesByIds, graphEventId, - height, id, isEventViewer = false, isSelectAllChecked, @@ -129,17 +127,11 @@ export const Body = React.memo( return ( <> {graphEventId && ( - + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 550a4adf713c..15fa13b1a08f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -42,7 +42,6 @@ interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; docValueFields: DocValueFields[]; - height?: number; id: string; isEventViewer?: boolean; sort: Sort; @@ -63,7 +62,6 @@ const StatefulBodyComponent = React.memo( docValueFields, eventIdToNoteIds, excludedRowRendererIds, - height, id, isEventViewer = false, isSelectAllChecked, @@ -199,7 +197,6 @@ const StatefulBodyComponent = React.memo( eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} graphEventId={graphEventId} - height={height} id={id} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} @@ -234,7 +231,6 @@ const StatefulBodyComponent = React.memo( prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && - prevProps.height === nextProps.height && prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index 4e1595eef984..3169201b12c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -68,7 +68,7 @@ const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ( height: `${height}px`, }, }))` - flex: 0; + flex: 0 0 auto; `; FooterContainer.displayName = 'FooterContainer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts index 3a01df8f48a3..cc979816f014 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts @@ -5,4 +5,3 @@ */ export const NOTES_PANEL_WIDTH = 1024; -export const NOTES_PANEL_HEIGHT = 750; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index 8f548f16cf1d..5b3bc72fc37c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -4,18 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { act as actDom } from 'react-dom/test-utils'; + import { renderHook, act } from '@testing-library/react-hooks'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { TimelineType } from '../../../../../common/types/timeline'; import { TestProviders } from '../../../../common/mock'; import { useCreateTimelineButton } from './use_create_timeline'; +const mockDispatch = jest.fn(); + jest.mock('react-redux', () => { const actual = jest.requireActual('react-redux'); return { ...actual, - useDispatch: jest.fn(), + useDispatch: () => mockDispatch, + useSelector: () => ({ + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + }), }; }); @@ -65,4 +76,35 @@ describe('useCreateTimelineButton', () => { expect(wrapper.find('[data-test-subj="timeline-new"]').exists()).toBeTruthy(); }); }); + + test('Make sure that timeline reset to the global date picker', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } + ); + + await waitForNextUpdate(); + const button = result.current.getButton({ outline: false, title: 'mock title' }); + actDom(() => { + const wrapper = mount(button); + wrapper.update(); + + wrapper.find('[data-test-subj="timeline-new"]').first().simulate('click'); + + expect(mockDispatch.mock.calls[0][0].type).toEqual( + 'x-pack/security_solution/local/timeline/CREATE_TIMELINE' + ); + expect(mockDispatch.mock.calls[1][0].type).toEqual( + 'x-pack/security_solution/local/inputs/ADD_GLOBAL_LINK_TO' + ); + expect(mockDispatch.mock.calls[2][0].type).toEqual( + 'x-pack/security_solution/local/inputs/ADD_TIMELINE_LINK_TO' + ); + expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/SET_RELATIVE_RANGE_DATE_PICKER' + ); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index f418491ac4e4..97f3b1df011f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { timelineActions } from '../../../store/timeline'; @@ -14,6 +14,7 @@ import { TimelineType, TimelineTypeLiteral, } from '../../../../../common/types/timeline'; +import { inputsActions, inputsSelectors } from '../../../../common/store/inputs'; export const useCreateTimelineButton = ({ timelineId, @@ -26,13 +27,12 @@ export const useCreateTimelineButton = ({ }) => { const dispatch = useDispatch(); const { timelineFullScreen, setTimelineFullScreen } = useFullScreen(); - + const globalTimeRange = useSelector(inputsSelectors.globalTimeRangeSelector); const createTimeline = useCallback( ({ id, show }) => { if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } - dispatch( timelineActions.createTimeline({ id, @@ -41,8 +41,25 @@ export const useCreateTimelineButton = ({ timelineType, }) ); + dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); + dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); + if (globalTimeRange.kind === 'absolute') { + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + ...globalTimeRange, + id: 'timeline', + }) + ); + } else if (globalTimeRange.kind === 'relative') { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + ...globalTimeRange, + id: 'timeline', + }) + ); + } }, - [dispatch, setTimelineFullScreen, timelineFullScreen, timelineType] + [dispatch, globalTimeRange, setTimelineFullScreen, timelineFullScreen, timelineType] ); const handleButtonClick = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index faeef432ea42..ebee0f5cae9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -104,6 +104,7 @@ export const updateTimeline = actionCreator<{ export const addTimeline = actionCreator<{ id: string; timeline: TimelineModel; + savedTimeline?: boolean; }>('ADD_TIMELINE'); export const setInsertTimeline = actionCreator('SET_INSERT_TIMELINE'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 7757794c6dc9..ad849c3a995b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -6,6 +6,7 @@ import { get, + getOr, has, merge as mergeObject, set, @@ -172,7 +173,7 @@ export const createTimelineEpic = (): Epic< myEpicTimelineId.setTimelineVersion(addNewTimeline.version); myEpicTimelineId.setTemplateTimelineId(addNewTimeline.templateTimelineId); myEpicTimelineId.setTemplateTimelineVersion(addNewTimeline.templateTimelineVersion); - return true; + return getOr(false, 'payload.savedTimeline', action); } else if ( timelineActionsType.includes(action.type) && !timelineObj.isLoading && diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 084f892369b5..161a31e2ec93 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -21,6 +21,7 @@ import { EndpointAppContext } from '../../types'; import { AgentService } from '../../../../../ingest_manager/server'; import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models'; import { findAllUnenrolledAgentIds } from './support/unenroll'; +import { findAgentIDsByStatus } from './support/agent_status'; interface HitSource { _source: HostMetadata; @@ -52,6 +53,21 @@ const getLogger = (endpointAppContext: EndpointAppContext): Logger => { return endpointAppContext.logFactory.get('metadata'); }; +/* Filters that can be applied to the endpoint fetch route */ +export const endpointFilters = schema.object({ + kql: schema.nullable(schema.string()), + host_status: schema.nullable( + schema.arrayOf( + schema.oneOf([ + schema.literal(HostStatus.ONLINE.toString()), + schema.literal(HostStatus.OFFLINE.toString()), + schema.literal(HostStatus.UNENROLLING.toString()), + schema.literal(HostStatus.ERROR.toString()), + ]) + ) + ), +}); + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const logger = getLogger(endpointAppContext); router.post( @@ -76,10 +92,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp ]) ) ), - /** - * filter to be applied, it could be a kql expression or discrete filter to be implemented - */ - filter: schema.nullable(schema.oneOf([schema.string()])), + filters: endpointFilters, }) ), }, @@ -103,12 +116,21 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp context.core.savedObjects.client ); + const statusIDs = req.body?.filters?.host_status?.length + ? await findAgentIDsByStatus( + agentService, + context.core.savedObjects.client, + req.body?.filters?.host_status + ) + : undefined; + const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, metadataIndexPattern, { unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index f3b832de9a78..29624b35d5c9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -27,7 +27,7 @@ import { HostStatus, } from '../../../../common/endpoint/types'; import { SearchResponse } from 'elasticsearch'; -import { registerEndpointRoutes } from './index'; +import { registerEndpointRoutes, endpointFilters } from './index'; import { createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -170,7 +170,7 @@ describe('test endpoint route', () => { }, ], - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); @@ -395,6 +395,53 @@ describe('test endpoint route', () => { }); }); +describe('Filters Schema Test', () => { + it('accepts a single host status', () => { + expect( + endpointFilters.validate({ + host_status: ['error'], + }) + ).toBeTruthy(); + }); + + it('accepts multiple host status filters', () => { + expect( + endpointFilters.validate({ + host_status: ['offline', 'unenrolling'], + }) + ).toBeTruthy(); + }); + + it('rejects invalid statuses', () => { + expect(() => + endpointFilters.validate({ + host_status: ['foobar'], + }) + ).toThrowError(); + }); + + it('accepts a KQL string', () => { + expect( + endpointFilters.validate({ + kql: 'whatever.field', + }) + ).toBeTruthy(); + }); + + it('accepts KQL + status', () => { + expect( + endpointFilters.validate({ + kql: 'thing.var', + host_status: ['online'], + }) + ).toBeTruthy(); + }); + + it('accepts no filters', () => { + expect(endpointFilters.validate({})).toBeTruthy(); + }); +}); + function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { return ({ took: 15, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index 266d522e8a41..e9eb7093a763 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -127,7 +127,7 @@ describe('query builder', () => { it('test default query params for all endpoints metadata when body filter is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( @@ -201,7 +201,7 @@ describe('query builder', () => { const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index f6385d271004..ba9be96201db 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -9,6 +9,7 @@ import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { unenrolledAgentIds?: string[]; + statusAgentIDs?: string[]; } export async function kibanaRequestToMetadataListESQuery( @@ -22,7 +23,11 @@ export async function kibanaRequestToMetadataListESQuery( const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request, queryBuilderOptions?.unenrolledAgentIds!), + query: buildQueryBody( + request, + queryBuilderOptions?.unenrolledAgentIds!, + queryBuilderOptions?.statusAgentIDs! + ), collapse: { field: 'host.id', inner_hits: { @@ -76,47 +81,52 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - unerolledAgentIds: string[] | undefined + unerolledAgentIds: string[] | undefined, + statusAgentIDs: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { - const filterUnenrolledAgents = unerolledAgentIds && unerolledAgentIds.length > 0; - if (typeof request?.body?.filter === 'string') { - const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); - return { - bool: { - must: filterUnenrolledAgents - ? [ - { - bool: { - must_not: { - terms: { - 'elastic.agent.id': unerolledAgentIds, - }, - }, - }, - }, - { - ...kqlQuery, - }, - ] - : [ - { - ...kqlQuery, - }, - ], - }, - }; - } - return filterUnenrolledAgents - ? { - bool: { + const filterUnenrolledAgents = + unerolledAgentIds && unerolledAgentIds.length > 0 + ? { must_not: { terms: { 'elastic.agent.id': unerolledAgentIds, }, }, + } + : null; + const filterStatusAgents = statusAgentIDs + ? { + must: { + terms: { + 'elastic.agent.id': statusAgentIDs, + }, }, } + : null; + + const idFilter = { + bool: { + ...filterUnenrolledAgents, + ...filterStatusAgents, + }, + }; + + if (request?.body?.filters?.kql) { + const kqlQuery = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(request.body.filters.kql) + ); + const q = []; + if (filterUnenrolledAgents || filterStatusAgents) { + q.push(idFilter); + } + q.push({ ...kqlQuery }); + return { + bool: { must: q }, + }; + } + return filterUnenrolledAgents || filterStatusAgents + ? idFilter : { match_all: {}, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts new file mode 100644 index 000000000000..a4b6b0750ec1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { findAgentIDsByStatus } from './agent_status'; +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { AgentService } from '../../../../../../ingest_manager/server/services'; +import { createMockAgentService } from '../../../mocks'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; + +describe('test filtering endpoint hosts by agent status', () => { + let mockSavedObjectClient: jest.Mocked; + let mockAgentService: jest.Mocked; + beforeEach(() => { + mockSavedObjectClient = savedObjectsClientMock.create(); + mockAgentService = createMockAgentService(); + }); + + it('will accept a valid status condition', async () => { + mockAgentService.listAgents.mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 0, + page: 1, + perPage: 10, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['online']); + expect(result).toBeDefined(); + }); + + it('will filter for offline hosts', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'id1' } as unknown) as Agent, ({ id: 'id2' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['offline']); + const offlineKuery = AgentStatusKueryHelper.buildKueryForOfflineAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(offlineKuery) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['id1', 'id2']); + }); + + it('will filter for multiple statuses', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'A' } as unknown) as Agent, ({ id: 'B' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, [ + 'unenrolling', + 'error', + ]); + const unenrollKuery = AgentStatusKueryHelper.buildKueryForUnenrollingAgents(); + const errorKuery = AgentStatusKueryHelper.buildKueryForErrorAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(`${unenrollKuery} OR ${errorKuery}`) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['A', 'B']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts new file mode 100644 index 000000000000..86f6d1a9a65e --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentService } from '../../../../../../ingest_manager/server'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { HostStatus } from '../../../../../common/endpoint/types'; + +const STATUS_QUERY_MAP = new Map([ + [HostStatus.ONLINE.toString(), AgentStatusKueryHelper.buildKueryForOnlineAgents()], + [HostStatus.OFFLINE.toString(), AgentStatusKueryHelper.buildKueryForOfflineAgents()], + [HostStatus.ERROR.toString(), AgentStatusKueryHelper.buildKueryForErrorAgents()], + [HostStatus.UNENROLLING.toString(), AgentStatusKueryHelper.buildKueryForUnenrollingAgents()], +]); + +export async function findAgentIDsByStatus( + agentService: AgentService, + soClient: SavedObjectsClientContract, + status: string[], + pageSize: number = 1000 +): Promise { + const helpers = status.map((s) => STATUS_QUERY_MAP.get(s)); + const searchOptions = (pageNum: number) => { + return { + page: pageNum, + perPage: pageSize, + showInactive: true, + kuery: `(fleet-agents.packages : "endpoint" AND (${helpers.join(' OR ')}))`, + }; + }; + + let page = 1; + + const result: string[] = []; + let hasMore = true; + + while (hasMore) { + const agents = await agentService.listAgents(soClient, searchOptions(page++)); + result.push(...agents.agents.map((agent: Agent) => agent.id)); + hasMore = agents.agents.length > 0; + } + return result; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index d99533e23f2c..902d287a09e4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -22,7 +22,13 @@ export class ChildrenQuery extends ResolverQuery { } protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { + const paginationFields = this.pagination.buildQueryFields('endgame.serial_event_id'); return { + collapse: { + field: 'endgame.unique_pid', + }, + size: paginationFields.size, + sort: paginationFields.sort, query: { bool: { filter: [ @@ -42,7 +48,7 @@ export class ChildrenQuery extends ResolverQuery { bool: { should: [ { - term: { 'event.type': 'process_start' }, + terms: { 'event.type': ['process_start', 'already_running'] }, }, { term: { 'event.action': 'fork_event' }, @@ -53,12 +59,30 @@ export class ChildrenQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id'), }; } protected query(entityIDs: string[]): JsonObject { + const paginationFields = this.pagination.buildQueryFieldsAsInterface('event.id'); return { + /** + * Using collapse here will only return a single event per occurrence of a process.entity_id. The events are sorted + * based on timestamp in ascending order so it will be the first event that ocurred. The actual type of event that + * we receive for this query doesn't really matter (whether it is a start, info, or exec for a particular entity_id). + * All this is trying to accomplish is removing duplicate events that indicate a process existed for a node. We + * only need to know that a process existed and it's it's ancestry array and the process.entity_id fields because + * we will use it to query for the next set of descendants. + * + * The reason it is important to only receive 1 event per occurrence of a process.entity_id is it allows us to avoid + * ES 10k limit most of the time. If instead we received multiple events with the same process.entity_id that would + * reduce the maximum number of unique children processes we could retrieve in a single query. + */ + collapse: { + field: 'process.entity_id', + }, + // do not set the search_after field because collapse does not work with it + size: paginationFields.size, + sort: paginationFields.sort, query: { bool: { filter: [ @@ -93,12 +117,11 @@ export class ChildrenQuery extends ResolverQuery { term: { 'event.kind': 'event' }, }, { - term: { 'event.type': 'start' }, + terms: { 'event.type': ['start', 'info', 'change'] }, }, ], }, }, - ...this.pagination.buildQueryFields('event.id'), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts index 33011078ee82..02cddc3ddcf6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts @@ -9,7 +9,6 @@ import { TypeOf } from '@kbn/config-schema'; import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; import { validateTree } from '../../../../common/endpoint/schema/resolver'; import { Fetcher } from './utils/fetch'; -import { Tree } from './utils/tree'; import { EndpointAppContext } from '../../types'; export function handleTree( @@ -17,42 +16,21 @@ export function handleTree( endpointAppContext: EndpointAppContext ): RequestHandler, TypeOf> { return async (context, req, res) => { - const { - params: { id }, - query: { - children, - ancestors, - events, - alerts, - afterAlert, - afterEvent, - afterChild, - legacyEndpointID: endpointID, - }, - } = req; try { const client = context.core.elasticsearch.legacy.client; - const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); + const fetcher = new Fetcher( + client, + req.params.id, + eventsIndexPattern, + alertsIndexPattern, + req.query.legacyEndpointID + ); - const [childrenNodes, ancestry, relatedEvents, relatedAlerts] = await Promise.all([ - fetcher.children(children, afterChild), - fetcher.ancestors(ancestors), - fetcher.events(events, afterEvent), - fetcher.alerts(alerts, afterAlert), - ]); - - const tree = new Tree(id, { - ancestry, - children: childrenNodes, - relatedEvents, - relatedAlerts, - }); - - const enrichedTree = await fetcher.stats(tree); + const tree = await fetcher.tree(req.query); return res.ok({ - body: enrichedTree.render(), + body: tree.render(), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 01dd59b2611d..78e4219aad75 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -10,12 +10,12 @@ import { TreeNode, } from '../../../../../common/endpoint/generate_data'; import { ChildrenNodesHelper } from './children_helper'; -import { eventId, isStart } from '../../../../../common/endpoint/models/event'; +import { eventId, isProcessRunning } from '../../../../../common/endpoint/models/event'; function getStartEvents(events: Event[]): Event[] { const startEvents: Event[] = []; for (const event of events) { - if (isStart(event)) { + if (isProcessRunning(event)) { startEvents.push(event); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index d3ca7a54c16d..ef487897e3b4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -7,7 +7,7 @@ import { entityId, parentEntityId, - isStart, + isProcessRunning, getAncestryAsArray, } from '../../../../../common/endpoint/models/event'; import { @@ -99,7 +99,7 @@ export class ChildrenNodesHelper { for (const event of startEvents) { const parentID = parentEntityId(event); const entityID = entityId(event); - if (parentID && entityID && isStart(event)) { + if (parentID && entityID && isProcessRunning(event)) { // don't actually add the start event to the node, because that'll be done in // a different call const childNode = this.getOrCreateChildNode(entityID); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts index 8aaf809405d6..ab610dc9776c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts @@ -66,6 +66,6 @@ export class ChildrenLifecycleQueryHandler implements SingleQueryHandler; + const pagination = this.buildQueryFieldsAsInterface(tiebreaker); + fields.sort = pagination.sort; + fields.size = pagination.size; + if (pagination.searchAfter) { + fields.search_after = pagination.searchAfter; } return fields; } diff --git a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts index 5f816b9ada54..10faa362363a 100644 --- a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts @@ -81,10 +81,16 @@ export const createNoteResolvers = ( return true; }, async persistNote(root, args, { req }) { - return libs.note.persistNote(req, args.noteId || null, args.version || null, { - ...args.note, - timelineId: args.note.timelineId || null, - }); + return libs.note.persistNote( + req, + args.noteId || null, + args.version || null, + { + ...args.note, + timelineId: args.note.timelineId || null, + }, + true + ); }, }, }); diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts index f4a18a40f7d4..9bd544f6942c 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts @@ -50,7 +50,7 @@ export const createTimelineResolvers = ( return libs.timeline.getAllTimeline( req, args.onlyUserFavorite || null, - args.pageInfo || null, + args.pageInfo, args.search || null, args.sort || null, args.status || null, diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 58a13a7115b7..573539e1bb54 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -178,8 +178,8 @@ export const timelineSchema = gql` } input PageInfoTimeline { - pageIndex: Float - pageSize: Float + pageIndex: Float! + pageSize: Float! } enum SortFieldTimeline { @@ -316,7 +316,7 @@ export const timelineSchema = gql` extend type Query { getOneTimeline(id: ID!): TimelineResult! - getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! + getAllTimeline(pageInfo: PageInfoTimeline!, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! } extend type Mutation { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index ca0732816aa4..fa55af351651 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -104,9 +104,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -425,11 +425,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2326,7 +2321,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; @@ -2802,7 +2797,7 @@ export namespace QueryResolvers { TContext = SiemContext > = Resolver; export interface GetAllTimelineArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json index ee39661ee9b1..acc6f7724d0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index 4132d03c2785..25274928aa2b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies certutil.exe making a network connection. Adversaries could abuse certutil.exe to download a certificate, or malware, from a remote URL.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json index eb8523b797dd..a2267755c737 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json index ddc9e9178213..4c79be6fe904 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json @@ -21,7 +21,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index a2936f3f0951..6be1f037f967 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json index ecbf268550b6..5b73f849dddc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json index 87f20525203f..2cf24d54b726 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json index f570b7fb3e94..ef20746fb1d8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json @@ -21,12 +21,15 @@ "https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html", "http://detectioninthe.cloud/credential_access/access_secret_in_secrets_manager/" ], - "risk_score": 21, + "risk_score": 73, "rule_id": "a00681e3-9ed6-447c-ab2c-be648821c622", - "severity": "low", + "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Data Protection", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json index 9abbe3de148d..d5b069f7b81e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json @@ -7,7 +7,8 @@ "Some normal use of this command may originate from server or network administrators engaged in network troubleshooting." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json index 861821d24b73..b22b74ebc53b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -4,7 +4,8 @@ ], "description": "Adversaries can add the 'hidden' attribute to files to hide them from the user in an attempt to evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json index 431d133845f0..e2ba81da917b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to disable the iptables or firewall service in an attempt to affect how a host is allowed to receive or send network traffic.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json index 13dd405c7932..4f4a9aacd79a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to disable the syslog service in an attempt to an attempt to disrupt event logging and evade detection by security controls.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index 67fb0b2e6755..5bcc4a00ccd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json index f60dede360b4..a17fd6d2702d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index 7c6ede8df734..cf09bc512916 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -4,7 +4,8 @@ ], "description": "Identifies attempts to clear Windows event log stores. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json index 78f4c9e853f6..b5e76b6ebfa3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json index f412ad9b2e2f..6ba9503edc26 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json index b76ea0944f85..3d31eead43c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json index 353067e6db83..22ceb35dfc85 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json index b70aa5cd11b5..95e357e56fe3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json index ba9f43651e32..0c82444dd939 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the fsutil.exe to delete the volume USNJRNL. This technique is used by attackers to eliminate evidence of files created during post-exploitation activities.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json index 79c2d4c25b7d..c76c5f20fa88 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the wbadmin.exe to delete the backup catalog. Ransomware and other malware may do this to prevent system recovery.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json index b9727e18dddc..b38ed94e132e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to clear the bash command line history in an attempt to evade detection or forensic investigations.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json index e8f5f1a8de1c..229a03de3960 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json @@ -4,7 +4,8 @@ ], "description": "Identifies potential attempts to disable Security-Enhanced Linux (SELinux), which is a Linux kernel security feature to support access control policies. Adversaries may disable security tools to avoid possible detection of their tools and activities.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json index 2b45f059ec8d..4800e87c180e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the netsh.exe to disable or weaken the local firewall. Attackers will use this command line tool to disable the firewall during troubleshooting or to enable network mobility.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json index a1b0ec0f01d2..809a9a187937 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json index 21ce4e498cca..8467b87f9983 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json index 056de9e5c003..075dd13d9819 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json @@ -4,7 +4,8 @@ ], "description": "Identifies the use of certutil.exe to encode or decode data. CertUtil is a native Windows component which is part of Certificate Services. CertUtil is often abused by attackers to encode or decode base64 data for stealthier command and control or exfiltration.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json index 814caee4e888..133863f8e214 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. It is quite unusual for this program to be started by an Office application like Word or Excel." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json index 6426f8722df3..85d348bb14be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json index b27dfced0f4f..38482c0a70fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index d7da758e57c6..7db683caf2bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json index 30d482e9b956..1c4666955dde 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. If a build system triggers this rule it can be exempted by process, user or host name." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json index 4aad56abd053..c375ea7b19b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json @@ -4,7 +4,8 @@ ], "description": "Malware or other files dropped or created on a system by an adversary may leave traces behind as to what was done within a network and how. Adversaries may remove these files over the course of an intrusion to keep their footprint low or remove them at the end as part of the post-intrusion cleanup process.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json index c630ad1eecec..22090e1a241e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json @@ -7,7 +7,8 @@ "Certain programs or applications may modify files or change ownership in writable directories. These can be exempted by username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json index 989eff90aaf0..c2590a2f062c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json index 3c1ea7ee229c..00491937e9aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json index 7202d9be3b8c..16a398011fc5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json @@ -7,7 +7,8 @@ "Certain tools may create hidden temporary files or directories upon installation or as part of their normal behavior. These events can be filtered by the process arguments, username, or process name values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json index f055ee44efb3..11781cb71959 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json @@ -7,7 +7,8 @@ "There is usually no reason to remove modules, but some buggy modules require it. These can be exempted by username. Note that some Linux distributions are not built to support the removal of modules at all." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index afa1467b1507..7d931725fa6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -4,7 +4,8 @@ ], "description": "Binaries signed with trusted digital certificates can execute on Windows systems protected by digital signature validation. Adversaries may use these binaries to 'live off the land' and execute malicious files that could bypass application allowlists and signature validation.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json index 801b60a2572e..1bffe7a1cfc2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of bcdedit.exe to delete boot configuration data. This tactic is sometimes used as by malware or an attacker as a destructive technique.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json index b1e8d0cd0d3e..13d0eb267f64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json @@ -28,7 +28,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json index 3166cc23ae72..f3cc5c2eec8a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of vssadmin.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json index 730879684a81..334276142ca4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of wmic.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json index b2092dc78b01..ef7667e34be3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json index ccec76b7f797..1e8e1bfa4224 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json index 14472f02280a..0e4bea426c59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json @@ -7,7 +7,8 @@ "Security tools and device drivers may run these programs in order to enumerate kernel modules. Use of these programs by ordinary users is uncommon. These can be exempted by process name or username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json index a2fe82c43b15..6ac2bbf35596 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json @@ -4,7 +4,8 @@ ], "description": "Identifies the SYSTEM account using the Net utility. The Net utility is a component of the Windows operating system. It is used in command line operations for control of users, groups, services, and network connections.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json index 94f09f73b454..e73aa5f4566a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json @@ -7,7 +7,8 @@ "Certain tools or automated software may enumerate hardware information. These tools can be exempted via user name or process arguments to eliminate potential noise." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json index a7833c4a0175..001718678713 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json @@ -7,7 +7,8 @@ "Security testing tools and frameworks may run this command. Some normal use of this command may originate from automation tools and frameworks." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json index 05601ec8ffb4..1466b4526815 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json @@ -1,5 +1,7 @@ { - "author": ["Elastic"], + "author": [ + "Elastic" + ], "description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Elastic Endpoint alerts.", "enabled": true, "exceptions_list": [ @@ -11,7 +13,9 @@ } ], "from": "now-10m", - "index": ["logs-endpoint.alerts-*"], + "index": [ + "logs-endpoint.alerts-*" + ], "language": "kuery", "license": "Elastic License", "max_signals": 10000, @@ -54,7 +58,10 @@ "value": "99" } ], - "tags": ["Elastic", "Endpoint"], + "tags": [ + "Elastic", + "Endpoint" + ], "timestamp_override": "event.ingested", "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index 97197be498a8..0ba6480fe42a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -7,7 +7,8 @@ "Administrators may use the command prompt for regular administrative tasks. It's important to baseline your environment for network connections being made from the command prompt to determine any abnormal use of this tool." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index 832ca1e1e7d3..2d3edb0f5f6c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -4,7 +4,8 @@ ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from PowerShell.exe.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json index e92ee45c0f3b..3a4b4915f3c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json @@ -4,7 +4,8 @@ ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index c75f77301e53..a2eb76b9831f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -4,7 +4,8 @@ ], "description": "Compiled HTML files (.chm) are commonly distributed as part of the Microsoft HTML Help system. Adversaries may conceal malicious code in a CHM file and deliver it to a victim for execution. CHM content is loaded by the HTML Help executable program (hh.exe).", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json index 9b50d99761ad..e43ab9de86ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of sc.exe to create, modify, or start services on remote hosts. This could be indicative of adversary lateral movement but will be noisy if commonly done by admins.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json index 192e35df1da3..9d480259d49d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json @@ -4,7 +4,8 @@ ], "description": "Identifies MsBuild.exe making outbound network connections. This may indicate adversarial activity as MsBuild is often leveraged by adversaries to execute code and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json index cb098086e332..cdef5f16e5cd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json @@ -4,7 +4,8 @@ ], "description": "Identifies mshta.exe making a network connection. This may indicate adversarial activity as mshta.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json index 9f1d2fc62fad..d501bda08c3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json @@ -4,7 +4,8 @@ ], "description": "Identifies msxsl.exe making a network connection. This may indicate adversarial activity as msxsl.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json index db96fe1bc1b5..e82b42869e44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json @@ -4,7 +4,8 @@ ], "description": "Identifies when a terminal (tty) is spawned via Perl. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json index a5ac6cffd237..e4c84fd3c3b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json @@ -7,7 +7,8 @@ "PsExec is a dual-use tool that can be used for benign or malicious activity. It's important to baseline your environment to determine the amount of noise to expect from this tool." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json index 59be6da19e93..3aa9ac20bba9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json @@ -4,7 +4,8 @@ ], "description": "Identifies when a terminal (tty) is spawned via Python. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index 262313782fe3..0a1ba97bd01e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -7,7 +7,8 @@ "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json index 6f9170f476d9..7305247192f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json @@ -4,7 +4,8 @@ ], "description": "Identifies a PowerShell process launched by either cscript.exe or wscript.exe. Observing Windows scripting processes executing a PowerShell script, may be indicative of malicious activity.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json index 1b5fd4e1f502..7ff8eb9424d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of frequently targeted Microsoft Office applications (Word, PowerPoint, Excel). These child processes are often launched during exploitation of Office applications or from documents with malicious macros.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json index f874b7e3f8e8..e923407765f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of Microsoft Outlook. These child processes are often associated with spear phishing activity.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json index 35206d130ea5..24a744ce3083 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of PDF reader applications. These child processes are often launched via exploitation of PDF applications or social engineering.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json index 43f1f8a5c9c6..529f2199e46d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json @@ -4,7 +4,8 @@ ], "description": "Identifies unusual instances of rundll32.exe making outbound network connections. This may indicate adversarial activity and may identify malicious DLLs.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json index b49d1b358cb8..69a25b3b24ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies network activity from unexpected system applications. This may indicate adversarial activity as these applications are often leveraged by adversaries to execute code and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json index 2c141da80e79..cae5d1b7e0f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json @@ -4,7 +4,8 @@ ], "description": "RegSvcs.exe and RegAsm.exe are Windows command line utilities that are used to register .NET Component Object Model (COM) assemblies. Adversaries can use RegSvcs.exe and RegAsm.exe to proxy execution of code through a trusted Windows utility.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json index a9f8ee1af8bf..5e3e44604e9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json index 25711afbb4c6..81d82670e794 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json index 8b627c48d290..6bc14f4e5af8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json @@ -1,5 +1,7 @@ { - "author": ["Elastic"], + "author": [ + "Elastic" + ], "description": "Generates a detection alert for each external alert written to the configured indices. Enabling this rule allows you to immediately begin investigating external alerts in the app.", "index": [ "apm-*-transaction*", @@ -51,7 +53,9 @@ "value": "99" } ], - "tags": ["Elastic"], + "tags": [ + "Elastic" + ], "timestamp_override": "event.ingested", "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json index 27e50313c8f8..ee434efa019d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json index 0bafa56c9af4..2de24a815525 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json index 74b5e0d93c44..9fe0d97ceda3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json index 59c659117c09..085acb9a2fb1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json index 10a1989ad642..f75e4d15f1e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json @@ -26,7 +26,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Data Protection", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json index 4aa0b355171f..68ad10977f4d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json index 25b300d33cce..aab2deff3a26 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json index 9ca8b7ed21ac..abcc6f65fbc6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json index e8343f1b7b7c..b0615cf03238 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json index 8c4387e60d28..d77533e5183a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json index 829d87c1964c..026e1e549b57 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json @@ -24,7 +24,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json index 7429c69fc317..bd20be0924d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json index 25bf7dd287d0..2344346c8d61 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json index b4850e77ae71..8a68b26abad2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies unexpected processes making network connections over port 445. Windows File Sharing is typically implemented over Server Message Block (SMB), which communicates between hosts using port 445. When legitimate, these network connections are established by the kernel. Processes making 445/tcp connections may be port scanners, exploits, or suspicious user-level processes moving laterally.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json index 27e5da09452e..2ea75dbd758c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json @@ -7,7 +7,8 @@ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json index 0273800c18d5..4379759608ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json @@ -7,7 +7,8 @@ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json index a842d8ef952f..24104439cd0e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json @@ -7,7 +7,8 @@ "Normal use of hping is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json index c1ce773c2aa4..73bf20a5a175 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json @@ -7,7 +7,8 @@ "Normal use of Iodine is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json index 98b262edfe6f..1895caf4dea8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json @@ -7,7 +7,8 @@ "Mknod is a Linux system program. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json index 30d34f245c6d..ac46bcbdbc08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json @@ -7,7 +7,8 @@ "Netcat is a dual-use tool that can be used for benign or malicious activity. Netcat is included in some Linux distributions so its presence is not necessarily suspicious. Some normal use of this program, while uncommon, may originate from scripts, automation tools, and frameworks." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json index 57f5fe57b0e0..2825dc28ad18 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json @@ -7,7 +7,8 @@ "Security testing tools and frameworks may run `Nmap` in the course of security auditing. Some normal use of this command may originate from security engineers and network or server administrators. Use of nmap by ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json index 086492edeb8a..234a09e9607b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json @@ -7,7 +7,8 @@ "Some normal use of this command may originate from security engineers and network or server administrators, but this is usually not routine or unannounced. Use of `Nping` by non-engineers or ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json index 09680fcf8e99..759622804444 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json @@ -7,7 +7,8 @@ "Build systems, like Jenkins, may start processes in the `/tmp` directory. These can be exempted by name or by username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json index 057d8ba9859a..cd38aff3f216 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json @@ -7,7 +7,8 @@ "Socat is a dual-use tool that can be used for benign or malicious activity. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json index 3dd18c8242a5..7fcb9f915c56 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json @@ -7,7 +7,8 @@ "Strace is a dual-use tool that can be used for benign or malicious activity. Some normal use of this command may originate from developers or SREs engaged in debugging or system call tracing." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json index 1d15db83bb18..b4c2d6522fb0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json index 6df2ed6cb34a..f64db94fbc7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json index e276166f6130..30e52eed8611 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json index bdfe7d25092b..18a72a331219 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Network", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json index e3e0d5fef7b2..b9c6e390effd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json index ad21ebe065f8..786fdd1ac16c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json index e92cf3d67d31..06089272f0e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json index d5f3995fb8bc..a9e6d2feef81 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json index c5d8e50d3dba..3392a1bff23b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json @@ -4,7 +4,8 @@ ], "description": "Detects writing executable files that will be automatically launched by Adobe on launch.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json index 5f6c006c5d17..49b9a7501a3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json index d3a66ef8d9c7..f289e8341a0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json index 7104cace1c5d..9393f7e4ef51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json index c38f71d8e00a..09aeed65f1ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json index 99bb07fe9660..229286d4e234 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json @@ -27,7 +27,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json index 9b2478b97fb3..b62384f5bd76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json index 48ed65caceda..e76379d171bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json @@ -7,7 +7,8 @@ "Security tools and device drivers may run these programs in order to load legitimate kernel modules. Use of these programs by ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json index b99690f78b2b..b9e7f941ee5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json @@ -7,7 +7,8 @@ "Legitimate scheduled tasks may be created during installation of new software." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json index 94a695a97a27..830d2d956125 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json @@ -27,7 +27,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json index 24ea80e10f5e..0cf6fcdb3875 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json @@ -7,7 +7,8 @@ "Network monitoring or management products may have a web server component that runs shell commands as part of normal behavior." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json index c3684006a49e..59715dae441f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json @@ -4,7 +4,8 @@ ], "description": "Windows services typically run as SYSTEM and can be used as a privilege escalation opportunity. Malware or penetration testers may run a shell as a service to gain SYSTEM permissions.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json index 5704f6d14bfe..7465751d5cd4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json @@ -4,7 +4,8 @@ ], "description": "Identifies attempts to create new local users. This is sometimes done by attackers to increase access to a system or domain.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json index 74c5376100b2..aff6df969d90 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json index 3738c04346e6..9550eea6ca6a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json @@ -4,7 +4,8 @@ ], "description": "An adversary may add the setgid bit to a file or directory in order to run a file with the privileges of the owning group. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setgid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json index 58dcd2d671f5..343426953add 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json @@ -4,7 +4,8 @@ ], "description": "An adversary may add the setuid bit to a file or directory in order to run a file with the privileges of the owning user. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setuid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index 9850d4d908b6..44b50c74bafe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -4,7 +4,8 @@ ], "description": "A sudoers file specifies the commands that users or groups can run and from which terminals. Adversaries can take advantage of these configurations to execute commands as other users or spawn processes with higher privileges.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json index d8b59804fecd..50692dae3856 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json @@ -4,7 +4,8 @@ ], "description": "Identifies User Account Control (UAC) bypass via eventvwr.exe. Attackers bypass UAC to stealthily execute code with elevated permissions.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index bc80953d0aa6..8f938c0ceee6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -4,7 +4,8 @@ ], "description": "Identifies Windows programs run from unexpected parent processes. This could indicate masquerading or other strange activity on a system.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json index 7ce54b00f211..e271f855e442 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh index 3dd8e7f1097f..f3b8a81f4086 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh @@ -14,13 +14,13 @@ STATUS=${1:-active} TIMELINE_TYPE=${2:-default} # Example get all timelines: -# ./timelines/find_timeline_by_filter.sh active +# sh ./timelines/find_timeline_by_filter.sh active # Example get all prepackaged timeline templates: # ./timelines/find_timeline_by_filter.sh immutable template # Example get all custom timeline templates: -# ./timelines/find_timeline_by_filter.sh active template +# sh ./timelines/find_timeline_by_filter.sh active template curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh index 335d1b8c8669..05a9e0bd1ac9 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh @@ -9,28 +9,11 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_all_timelines.sh +# Example: sh ./timelines/get_all_timelines.sh + curl -s -k \ -H "Content-Type: application/json" \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ - -d '{ - "operationName": "GetAllTimeline", - "variables": { - "onlyUserFavorite": false, - "pageInfo": { - "pageIndex": null, - "pageSize": null - }, - "search": "", - "sort": { - "sortField": "updated", - "sortOrder": "desc" - }, - "status": "active", - "timelineType": null - }, - "query": "query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n" -}' | jq . - + -X GET "${KIBANA_URL}${SPACE_URL}/api/timeline" \ + | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh index 0c0694c0591f..13184ac6c6d5 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_id.sh {timeline_id} +# Example: sh ./timelines/get_timeline_by_id.sh {timeline_id} curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh index 36862b519130..87eddfbe6b9d 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} +# Example: sh ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 513d6a93d1b5..95ec753c21fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -109,6 +109,24 @@ export const sampleDocNoSortId = ( sort: [], }); +export const sampleDocSeverity = ( + severity?: Array | string | number | null +): SignalSourceHit => ({ + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _version: 1, + _id: sampleIdGuid, + _source: { + someKey: 'someValue', + '@timestamp': '2020-04-20T21:27:45+0000', + event: { + severity: severity ?? 100, + }, + }, + sort: [], +}); + export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({ took: 10, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 8c39a254e426..3334cc17b905 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -126,7 +126,7 @@ describe('filterEventsAgainstList', () => { ); expect(res.hits.hits.length).toEqual(2); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(['3.3.3.3', '7.7.7.7']).toEqual(ipVals); }); @@ -188,7 +188,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(6); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(['1.1.1.1', '3.3.3.3', '5.5.5.5', '7.7.7.7', '8.8.8.8', '9.9.9.9']).toEqual(ipVals); }); @@ -247,7 +247,7 @@ describe('filterEventsAgainstList', () => { buildRuleMessage, }); expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(res.hits.hits.length).toEqual(7); @@ -324,7 +324,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(8); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect([ '1.1.1.1', @@ -386,7 +386,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(9); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect([ '1.1.1.1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts index 80950335934f..fb1d51364ab3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { sampleDocNoSortId, sampleDocSeverity } from '../__mocks__/es_results'; import { buildSeverityFromMapping } from './build_severity_from_mapping'; describe('buildSeverityFromMapping', () => { @@ -12,7 +12,7 @@ describe('buildSeverityFromMapping', () => { jest.clearAllMocks(); }); - test('severity defaults to provided if mapping is incomplete', () => { + test('severity defaults to provided if mapping is undefined', () => { const severity = buildSeverityFromMapping({ doc: sampleDocNoSortId(), severity: 'low', @@ -22,5 +22,45 @@ describe('buildSeverityFromMapping', () => { expect(severity).toEqual({ severity: 'low', severityMeta: {} }); }); + test('severity is overridden to highest matched mapping', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocSeverity(23), + severity: 'low', + severityMapping: [ + { field: 'event.severity', operator: 'equals', value: '23', severity: 'critical' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'low' }, + { field: 'event.severity', operator: 'equals', value: '11', severity: 'critical' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, + ], + }); + + expect(severity).toEqual({ + severity: 'critical', + severityMeta: { + severityOverrideField: 'event.severity', + }, + }); + }); + + test('severity is overridden when field is event.severity and source value is number', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocSeverity(23), + severity: 'low', + severityMapping: [ + { field: 'event.severity', operator: 'equals', value: '13', severity: 'low' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, + { field: 'event.severity', operator: 'equals', value: '33', severity: 'high' }, + { field: 'event.severity', operator: 'equals', value: '43', severity: 'critical' }, + ], + }); + + expect(severity).toEqual({ + severity: 'medium', + severityMeta: { + severityOverrideField: 'event.severity', + }, + }); + }); + // TODO: Enhance... }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts index a3c4f47b491b..c0a62a2cc887 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -24,6 +24,13 @@ interface BuildSeverityFromMappingReturn { severityMeta: Meta; // TODO: Stricter types } +const severitySortMapping = { + low: 0, + medium: 1, + high: 2, + critical: 3, +}; + export const buildSeverityFromMapping = ({ doc, severity, @@ -31,10 +38,24 @@ export const buildSeverityFromMapping = ({ }: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { if (severityMapping != null && severityMapping.length > 0) { let severityMatch: SeverityMappingItem | undefined; - severityMapping.forEach((mapping) => { - // TODO: Expand by verifying fieldType from index via doc._index - const mappedValue = get(mapping.field, doc._source); - if (mapping.value === mappedValue) { + + // Sort the SeverityMapping from low to high, so last match (highest severity) is used + const severityMappingSorted = severityMapping.sort( + (a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity] + ); + + severityMappingSorted.forEach((mapping) => { + const docValue = get(mapping.field, doc._source); + // TODO: Expand by verifying fieldType from index via doc._index + // Till then, explicit parsing of event.severity (long) to number. If not ECS, this could be + // another datatype, but until we can lookup datatype we must assume number for the Elastic + // Endpoint Security rule to function correctly + let parsedMappingValue: string | number = mapping.value; + if (mapping.field === 'event.severity') { + parsedMappingValue = Math.floor(Number(parsedMappingValue)); + } + + if (parsedMappingValue === docValue) { severityMatch = { ...mapping }; } }); diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts index ee0d98c45c44..10dcb7ee7e74 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts @@ -26,6 +26,7 @@ export const buildHostOverviewQuery = ({ { range: { [timestamp]: { + format: 'strict_date_optional_time', gte: from, lte: to, }, diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts index 6493a3e05bfc..6249e60d9a2b 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts @@ -45,7 +45,7 @@ describe('elasticsearch_adapter', () => { describe('#getUsers', () => { test('will format edges correctly', () => { - // @ts-ignore Re-work `DatabaseSearchResponse` types as mock ES Response won't match + // @ts-expect-error Re-work `DatabaseSearchResponse` types as mock ES Response won't match const edges = getUsersEdges(mockUsersData); expect(edges).toEqual(mockFormattedUsersEdges); }); diff --git a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts b/x-pack/plugins/security_solution/server/lib/note/saved_object.ts index bf6090f0337f..0b043d4e2fdd 100644 --- a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/note/saved_object.ts @@ -49,7 +49,8 @@ export interface Note { request: FrameworkRequest, noteId: string | null, version: string | null, - note: SavedNote + note: SavedNote, + overrideOwner: boolean ) => Promise; convertSavedObjectToSavedNote: ( savedObject: unknown, @@ -136,7 +137,8 @@ export const persistNote = async ( request: FrameworkRequest, noteId: string | null, version: string | null, - note: SavedNote + note: SavedNote, + overrideOwner: boolean = true ): Promise => { try { const savedObjectsClient = request.context.core.savedObjects.client; @@ -163,14 +165,14 @@ export const persistNote = async ( note: convertSavedObjectToSavedNote( await savedObjectsClient.create( noteSavedObjectType, - pickSavedNote(noteId, note, request.user) + overrideOwner ? pickSavedNote(noteId, note, request.user) : note ), timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined ), }; } - // Update new note + // Update existing note const existingNote = await getSavedNote(request, noteId); return { @@ -180,7 +182,7 @@ export const persistNote = async ( await savedObjectsClient.update( noteSavedObjectType, noteId, - pickSavedNote(noteId, note, request.user), + overrideOwner ? pickSavedNote(noteId, note, request.user) : note, { version: existingNote.version || undefined, } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index b54d12d7efce..f888675b6041 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -37,7 +37,6 @@ const getTimelineTypeAndStatus = ( status: TimelineStatus | null = TimelineStatus.active ) => { // TODO: Added to support legacy TimelineType.draft, can be removed in 7.10 - // @ts-ignore if (timelineType === 'draft') { return { timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 0b10018de5bb..245146dda183 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -1198,3 +1198,21 @@ export const mockCheckTimelinesStatusAfterInstallResult = { }, ], }; + +export const mockSavedObject = { + type: 'siem-ui-timeline', + id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + attributes: { + savedQueryId: null, + + status: 'immutable', + + excludedRowRendererIds: [], + ...mockGetTemplateTimelineValue, + }, + references: [], + updated_at: '2020-07-21T12:03:08.901Z', + version: 'WzAsMV0=', + namespaces: ['default'], + score: 0.9444616, +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index e3aeff280678..c5d69398b7f0 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -175,11 +175,11 @@ export const cleanDraftTimelinesRequest = (timelineType: TimelineType) => }, }); -export const getTimelineByIdRequest = (query: GetTimelineByIdSchemaQuery) => +export const getTimelineRequest = (query?: GetTimelineByIdSchemaQuery) => requestMock.create({ method: 'get', path: TIMELINE_URL, - query, + query: query ?? {}, }); export const installPrepackedTimelinesRequest = () => diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts index 5bc4bec45dfb..7abcb390d022 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts @@ -20,6 +20,7 @@ import { TimelineStatusActions, } from './utils/common'; import { createTimelines } from './utils/create_timelines'; +import { DEFAULT_ERROR } from './utils/failure_cases'; export const createTimelinesRoute = ( router: IRouter, @@ -85,7 +86,7 @@ export const createTimelinesRoute = ( return siemResponse.error( compareTimelinesStatus.checkIsFailureCases(TimelineStatusActions.create) || { statusCode: 405, - body: 'update timeline error', + body: DEFAULT_ERROR, } ); } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts similarity index 66% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts index 30528f8563ab..6f99739ae2e2 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts @@ -10,19 +10,24 @@ import { requestContextMock, createMockConfig, } from '../../detection_engine/routes/__mocks__'; +import { getAllTimeline } from '../saved_object'; import { mockGetCurrentUser } from './__mocks__/import_timelines'; -import { getTimelineByIdRequest } from './__mocks__/request_responses'; +import { getTimelineRequest } from './__mocks__/request_responses'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; -import { getTimelineByIdRoute } from './get_timeline_by_id_route'; +import { getTimelineRoute } from './get_timeline_route'; jest.mock('./utils/create_timelines', () => ({ getTimeline: jest.fn(), getTemplateTimeline: jest.fn(), })); -describe('get timeline by id', () => { +jest.mock('../saved_object', () => ({ + getAllTimeline: jest.fn(), +})); + +describe('get timeline', () => { let server: ReturnType; let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); @@ -41,15 +46,12 @@ describe('get timeline by id', () => { authz: {}, } as unknown) as SecurityPluginSetup; - getTimelineByIdRoute(server.router, createMockConfig(), securitySetup); + getTimelineRoute(server.router, createMockConfig(), securitySetup); }); test('should call getTemplateTimeline if templateTimelineId is given', async () => { const templateTimelineId = '123'; - await server.inject( - getTimelineByIdRequest({ template_timeline_id: templateTimelineId }), - context - ); + await server.inject(getTimelineRequest({ template_timeline_id: templateTimelineId }), context); expect((getTemplateTimeline as jest.Mock).mock.calls[0][1]).toEqual(templateTimelineId); }); @@ -57,8 +59,16 @@ describe('get timeline by id', () => { test('should call getTimeline if id is given', async () => { const id = '456'; - await server.inject(getTimelineByIdRequest({ id }), context); + await server.inject(getTimelineRequest({ id }), context); expect((getTimeline as jest.Mock).mock.calls[0][1]).toEqual(id); }); + + test('should call getAllTimeline if nither templateTimelineId nor id is given', async () => { + (getAllTimeline as jest.Mock).mockResolvedValue({ totalCount: 3 }); + + await server.inject(getTimelineRequest(), context); + + expect(getAllTimeline as jest.Mock).toHaveBeenCalledTimes(2); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts similarity index 67% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts index c4957b9d4b9e..f36adb648cc0 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts @@ -17,8 +17,10 @@ import { buildSiemResponse, transformError } from '../../detection_engine/routes import { buildFrameworkRequest } from './utils/common'; import { getTimelineByIdSchemaQuery } from './schemas/get_timeline_by_id_schema'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { getAllTimeline } from '../saved_object'; +import { TimelineStatus } from '../../../../common/types/timeline'; -export const getTimelineByIdRoute = ( +export const getTimelineRoute = ( router: IRouter, config: ConfigType, security: SetupPlugins['security'] @@ -34,12 +36,33 @@ export const getTimelineByIdRoute = ( async (context, request, response) => { try { const frameworkRequest = await buildFrameworkRequest(context, security, request); - const { template_timeline_id: templateTimelineId, id } = request.query; + const query = request.query ?? {}; + const { template_timeline_id: templateTimelineId, id } = query; let res = null; - if (templateTimelineId != null) { + if (templateTimelineId != null && id == null) { res = await getTemplateTimeline(frameworkRequest, templateTimelineId); - } else if (id != null) { + } else if (templateTimelineId == null && id != null) { res = await getTimeline(frameworkRequest, id); + } else if (templateTimelineId == null && id == null) { + const tempResult = await getAllTimeline( + frameworkRequest, + false, + { pageSize: 1, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); + + res = await getAllTimeline( + frameworkRequest, + false, + { pageSize: tempResult?.totalCount ?? 0, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); } return response.ok({ body: res ?? {} }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index b817896e901c..2ad6c5d6fff6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -46,6 +46,7 @@ describe('import timelines', () => { let mockPersistTimeline: jest.Mock; let mockPersistPinnedEventOnTimeline: jest.Mock; let mockPersistNote: jest.Mock; + let mockGetNote: jest.Mock; let mockGetTupleDuplicateErrorsAndUniqueTimeline: jest.Mock; beforeEach(() => { @@ -69,6 +70,7 @@ describe('import timelines', () => { mockPersistTimeline = jest.fn(); mockPersistPinnedEventOnTimeline = jest.fn(); mockPersistNote = jest.fn(); + mockGetNote = jest.fn(); mockGetTupleDuplicateErrorsAndUniqueTimeline = jest.fn(); jest.doMock('../create_timelines_stream_from_ndjson', () => { @@ -113,6 +115,37 @@ describe('import timelines', () => { jest.doMock('../../note/saved_object', () => { return { persistNote: mockPersistNote, + getNote: mockGetNote + .mockResolvedValueOnce({ + noteId: 'd2649d40-6bc5-11ea-86f0-5db0048c6086', + version: 'WzExNjQsMV0=', + eventId: undefined, + note: 'original note', + created: '1584830796960', + createdBy: 'original author A', + updated: '1584830796960', + updatedBy: 'original author A', + }) + .mockResolvedValueOnce({ + noteId: '73ac2370-6bc2-11ea-a90b-f5341fb7a189', + version: 'WzExMjgsMV0=', + eventId: 'ZaAi8nAB5OldxqFfdhke', + note: 'original event note', + created: '1584830796960', + createdBy: 'original author B', + updated: '1584830796960', + updatedBy: 'original author B', + }) + .mockResolvedValue({ + noteId: 'f7b71620-6bc2-11ea-a0b6-33c7b2a78885', + version: 'WzExMzUsMV0=', + eventId: 'ZaAi8nAB5OldxqFfdhke', + note: 'event note2', + created: '1584830796960', + createdBy: 'angela', + updated: '1584830796960', + updatedBy: 'angela', + }), }; }); @@ -213,6 +246,14 @@ describe('import timelines', () => { ); }); + test('should Check if note exists', async () => { + const mockRequest = getImportTimelinesRequest(); + await server.inject(mockRequest, context); + expect(mockGetNote.mock.calls[0][1]).toEqual( + mockUniqueParsedObjects[0].globalNotes[0].noteId + ); + }); + test('should Create notes', async () => { const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); @@ -237,20 +278,67 @@ describe('import timelines', () => { expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTimeline.version); }); - test('should provide new notes when Creating notes for a timeline', async () => { + test('should provide new notes with original author info when Creating notes for a timeline', async () => { + const mockRequest = getImportTimelinesRequest(); + await server.inject(mockRequest, context); + expect(mockPersistNote.mock.calls[0][3]).toEqual({ + eventId: undefined, + note: 'original note', + created: '1584830796960', + createdBy: 'original author A', + updated: '1584830796960', + updatedBy: 'original author A', + timelineId: mockCreatedTimeline.savedObjectId, + }); + expect(mockPersistNote.mock.calls[1][3]).toEqual({ + eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId, + note: 'original event note', + created: '1584830796960', + createdBy: 'original author B', + updated: '1584830796960', + updatedBy: 'original author B', + timelineId: mockCreatedTimeline.savedObjectId, + }); + expect(mockPersistNote.mock.calls[2][3]).toEqual({ + eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId, + note: 'event note2', + created: '1584830796960', + createdBy: 'angela', + updated: '1584830796960', + updatedBy: 'angela', + timelineId: mockCreatedTimeline.savedObjectId, + }); + }); + + test('should keep current author if note does not exist when Creating notes for a timeline', async () => { + mockGetNote.mockReset(); + mockGetNote.mockRejectedValue(new Error()); + const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ + created: mockUniqueParsedObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedObjects[0].globalNotes[0].updatedBy, eventId: undefined, note: mockUniqueParsedObjects[0].globalNotes[0].note, timelineId: mockCreatedTimeline.savedObjectId, }); expect(mockPersistNote.mock.calls[1][3]).toEqual({ + created: mockUniqueParsedObjects[0].eventNotes[0].created, + createdBy: mockUniqueParsedObjects[0].eventNotes[0].createdBy, + updated: mockUniqueParsedObjects[0].eventNotes[0].updated, + updatedBy: mockUniqueParsedObjects[0].eventNotes[0].updatedBy, eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId, note: mockUniqueParsedObjects[0].eventNotes[0].note, timelineId: mockCreatedTimeline.savedObjectId, }); expect(mockPersistNote.mock.calls[2][3]).toEqual({ + created: mockUniqueParsedObjects[0].eventNotes[1].created, + createdBy: mockUniqueParsedObjects[0].eventNotes[1].createdBy, + updated: mockUniqueParsedObjects[0].eventNotes[1].updated, + updatedBy: mockUniqueParsedObjects[0].eventNotes[1].updatedBy, eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId, note: mockUniqueParsedObjects[0].eventNotes[1].note, timelineId: mockCreatedTimeline.savedObjectId, @@ -573,6 +661,10 @@ describe('import timeline templates', () => { expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note, + created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updatedBy, timelineId: mockCreatedTemplateTimeline.savedObjectId, }); }); @@ -721,6 +813,10 @@ describe('import timeline templates', () => { expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note, + created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updatedBy, timelineId: mockCreatedTemplateTimeline.savedObjectId, }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts index 2c6098bc7550..65c956ed6044 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import * as rt from 'io-ts'; +import { unionWithNullType } from '../../../../../common/utility_types'; -export const getTimelineByIdSchemaQuery = rt.partial({ - template_timeline_id: rt.string, - id: rt.string, -}); +export const getTimelineByIdSchemaQuery = unionWithNullType( + rt.partial({ + template_timeline_id: rt.string, + id: rt.string, + }) +); export type GetTimelineByIdSchemaQuery = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts index a622ee9b1570..07ce9a7336d4 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts @@ -34,7 +34,6 @@ export const updateTimelinesRoute = ( tags: ['access:securitySolution'], }, }, - // eslint-disable-next-line complexity async (context, request, response) => { const siemResponse = buildSiemResponse(response); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts index 2ce2c37d4fa3..b5aa24336b2d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts @@ -35,7 +35,7 @@ export const checkTimelinesStatus = async ( try { readStream = await getReadables(dataPath); - timeline = await getExistingPrepackagedTimelines(frameworkRequest, false); + timeline = await getExistingPrepackagedTimelines(frameworkRequest); } catch (err) { return { timelinesToInstall: [], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts index cdedffbbd945..6bdecb5d80ec 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts @@ -45,26 +45,56 @@ export const savePinnedEvents = ( ) ); -export const saveNotes = ( +const getNewNote = async ( frameworkRequest: FrameworkRequest, + note: NoteResult, timelineSavedObjectId: string, - timelineVersion?: string | null, - existingNoteIds?: string[], - newNotes?: NoteResult[] -) => { - return Promise.all( - newNotes?.map((note) => { - const newNote: SavedNote = { + overrideOwner: boolean +): Promise => { + let savedNote = note; + try { + savedNote = await noteLib.getNote(frameworkRequest, note.noteId); + // eslint-disable-next-line no-empty + } catch (e) {} + return overrideOwner + ? { eventId: note.eventId, note: note.note, timelineId: timelineSavedObjectId, + } + : { + eventId: savedNote.eventId, + note: savedNote.note, + created: savedNote.created, + createdBy: savedNote.createdBy, + updated: savedNote.updated, + updatedBy: savedNote.updatedBy, + timelineId: timelineSavedObjectId, }; +}; +export const saveNotes = async ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + timelineVersion?: string | null, + existingNoteIds?: string[], + newNotes?: NoteResult[], + overrideOwner: boolean = true +) => { + return Promise.all( + newNotes?.map(async (note) => { + const newNote = await getNewNote( + frameworkRequest, + note, + timelineSavedObjectId, + overrideOwner + ); return noteLib.persistNote( frameworkRequest, - existingNoteIds?.find((nId) => nId === note.noteId) ?? null, + overrideOwner ? existingNoteIds?.find((nId) => nId === note.noteId) ?? null : null, timelineVersion ?? null, - newNote + newNote, + overrideOwner ); }) ?? [] ); @@ -75,12 +105,18 @@ interface CreateTimelineProps { timeline: SavedTimeline; timelineSavedObjectId?: string | null; timelineVersion?: string | null; + overrideNotesOwner?: boolean; pinnedEventIds?: string[] | null; notes?: NoteResult[]; existingNoteIds?: string[]; isImmutable?: boolean; } +/** allow overrideNotesOwner means overriding by current username, + * disallow overrideNotesOwner means keep the original username. + * overrideNotesOwner = false only happens when import timeline templates, + * as we want to keep the original creator for notes + **/ export const createTimelines = async ({ frameworkRequest, timeline, @@ -90,6 +126,7 @@ export const createTimelines = async ({ notes = [], existingNoteIds = [], isImmutable, + overrideNotesOwner = true, }: CreateTimelineProps): Promise => { const responseTimeline = await saveTimelines( frameworkRequest, @@ -119,7 +156,8 @@ export const createTimelines = async ({ timelineSavedObjectId ?? newTimelineSavedObjectId, newTimelineVersion, existingNoteIds, - notes + notes, + overrideNotesOwner ), ]; } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts index 6f194c3b8538..79ebf6280a19 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts @@ -20,7 +20,7 @@ import { FrameworkRequest } from '../../../framework'; import * as noteLib from '../../../note/saved_object'; import * as pinnedEventLib from '../../../pinned_event/saved_object'; -import { getTimelines } from '../../saved_object'; +import { getSelectedTimelines } from '../../saved_object'; const getGlobalEventNotesByTimelineId = (currentNotes: NoteSavedObject[]): ExportedNotes => { const initialNotes: ExportedNotes = { @@ -55,7 +55,7 @@ const getTimelinesFromObjects = async ( request: FrameworkRequest, ids?: string[] | null ): Promise> => { - const { timelines, errors } = await getTimelines(request, ids); + const { timelines, errors } = await getSelectedTimelines(request, ids); const exportedIds = timelines.map((t) => t.savedObjectId); const [notes, pinnedEvents] = await Promise.all([ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts index b926819d66c9..e358ad9dbb57 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts @@ -37,6 +37,7 @@ export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'You cannot convert a Timeline template to a timeline, or a timeline to a Timeline template.'; export const UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE = 'You cannot update a timeline via imports. Use the UI to modify existing timelines.'; +export const DEFAULT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; const isUpdatingStatus = ( isHandlingTemplateTimeline: boolean, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts deleted file mode 100644 index 1dac773ad6fd..000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.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 { FrameworkRequest } from '../../../framework'; -import { getTimelines as getSelectedTimelines } from '../../saved_object'; -import { TimelineSavedObject } from '../../../../../common/types/timeline'; - -export const getTimelines = async ( - frameworkRequest: FrameworkRequest, - ids: string[] -): Promise<{ timeline: TimelineSavedObject[] | null; error: string | null }> => { - try { - const timelines = await getSelectedTimelines(frameworkRequest, ids); - const existingTimelineIds = timelines.timelines.map((timeline) => timeline.savedObjectId); - const errorMsg = timelines.errors.reduce( - (acc, curr) => (acc ? `${acc}, ${curr.message}` : curr.message), - '' - ); - if (existingTimelineIds.length > 0) { - const message = existingTimelineIds.join(', '); - return { - timeline: timelines.timelines, - error: errorMsg ? `${message} found, ${errorMsg}` : null, - }; - } else { - return { timeline: null, error: errorMsg }; - } - } catch (e) { - return e.message; - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index 996dc5823691..f62f02cc7bba 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -26,6 +26,7 @@ import { createPromiseFromStreams } from '../../../../../../../../src/legacy/uti import { getTupleDuplicateErrorsAndUniqueTimeline } from './get_timelines_from_stream'; import { CompareTimelinesStatus } from './compare_timelines_status'; import { TimelineStatusActions } from './common'; +import { DEFAULT_ERROR } from './failure_cases'; export type ImportedTimeline = SavedTimeline & { savedObjectId: string | null; @@ -77,8 +78,25 @@ export const timelineSavedObjectOmittedFields = [ 'version', ]; +export const setTimeline = ( + parsedTimelineObject: Partial, + parsedTimeline: ImportedTimeline, + isTemplateTimeline: boolean +) => { + return { + ...parsedTimelineObject, + status: + parsedTimeline.status === TimelineStatus.draft + ? TimelineStatus.active + : parsedTimeline.status ?? TimelineStatus.active, + templateTimelineVersion: isTemplateTimeline + ? parsedTimeline.templateTimelineVersion ?? 1 + : null, + templateTimelineId: isTemplateTimeline ? parsedTimeline.templateTimelineId ?? uuid.v4() : null, + }; +}; + const CHUNK_PARSED_OBJECT_SIZE = 10; -const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; export const importTimelines = async ( file: Readable, @@ -151,18 +169,11 @@ export const importTimelines = async ( // create timeline / timeline template newTimeline = await createTimelines({ frameworkRequest, - timeline: { - ...parsedTimelineObject, - status: - status === TimelineStatus.draft - ? TimelineStatus.active - : status ?? TimelineStatus.active, - templateTimelineVersion: isTemplateTimeline ? templateTimelineVersion : null, - templateTimelineId: isTemplateTimeline ? templateTimelineId ?? uuid.v4() : null, - }, + timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, + overrideNotesOwner: false, }); resolve({ @@ -176,7 +187,7 @@ export const importTimelines = async ( const errorMessage = compareTimelinesStatus.checkIsFailureCases( TimelineStatusActions.createViaImport ); - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + const message = errorMessage?.body ?? DEFAULT_ERROR; resolve( createBulkErrorObject({ @@ -196,6 +207,7 @@ export const importTimelines = async ( notes: globalNotes, existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, isImmutable, + overrideNotesOwner: false, }); resolve({ @@ -208,7 +220,7 @@ export const importTimelines = async ( TimelineStatusActions.updateViaImport ); - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + const message = errorMessage?.body ?? DEFAULT_ERROR; resolve( createBulkErrorObject({ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts index 3c4343b64289..0ef83bb84c4c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts @@ -3,8 +3,30 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { FrameworkRequest } from '../framework'; +import { mockGetTimelineValue, mockSavedObject } from './routes/__mocks__/import_timelines'; -import { convertStringToBase64 } from './saved_object'; +import { + convertStringToBase64, + getExistingPrepackagedTimelines, + getAllTimeline, + AllTimelinesResponse, +} from './saved_object'; +import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; +import { getNotesByTimelineId } from '../note/saved_object'; +import { getAllPinnedEventsByTimelineId } from '../pinned_event/saved_object'; + +jest.mock('./convert_saved_object_to_savedtimeline', () => ({ + convertSavedObjectToSavedTimeline: jest.fn(), +})); + +jest.mock('../note/saved_object', () => ({ + getNotesByTimelineId: jest.fn().mockResolvedValue([]), +})); + +jest.mock('../pinned_event/saved_object', () => ({ + getAllPinnedEventsByTimelineId: jest.fn().mockResolvedValue([]), +})); describe('saved_object', () => { describe('convertStringToBase64', () => { @@ -22,4 +44,210 @@ describe('saved_object', () => { expect(convertStringToBase64('')).toBe(''); }); }); + + describe('getExistingPrepackagedTimelines', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + + beforeEach(() => { + mockFindSavedObject = jest.fn().mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if countsOnly is true', async () => { + const contsOnly = true; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if countsOnly is false', async () => { + const contsOnly = false; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if pageInfo is given', async () => { + const contsOnly = false; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + await getExistingPrepackagedTimelines(mockRequest, contsOnly, pageInfo); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 10, + type: 'siem-ui-timeline', + }); + }); + }); + + describe('getAllTimeline', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + let result = (null as unknown) as AllTimelinesResponse; + beforeEach(async () => { + (convertSavedObjectToSavedTimeline as jest.Mock).mockReturnValue(mockGetTimelineValue); + mockFindSavedObject = jest + .fn() + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [], total: 0 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + + result = await getAllTimeline(mockRequest, false, pageInfo, null, null, null, null); + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if no filters applys', async () => { + expect(mockFindSavedObject.mock.calls[0][0]).toEqual({ + filter: 'not siem-ui-timeline.attributes.status: draft', + page: pageInfo.pageIndex, + perPage: pageInfo.pageSize, + type: 'siem-ui-timeline', + sortField: undefined, + sortOrder: undefined, + search: undefined, + searchFields: ['title', 'description'], + }); + }); + + test('should send correct options for counts of default timelines', async () => { + expect(mockFindSavedObject.mock.calls[1][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of timeline templates', async () => { + expect(mockFindSavedObject.mock.calls[2][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of Elastic prebuilt templates', async () => { + expect(mockFindSavedObject.mock.calls[3][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of custom templates', async () => { + expect(mockFindSavedObject.mock.calls[4][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of favorite timeline', async () => { + expect(mockFindSavedObject.mock.calls[5][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + search: ' dXNlcm5hbWU=', + searchFields: ['title', 'description', 'favorite.keySearch'], + type: 'siem-ui-timeline', + }); + }); + + test('should call getNotesByTimelineId', async () => { + expect((getNotesByTimelineId as jest.Mock).mock.calls[0][1]).toEqual(mockSavedObject.id); + }); + + test('should call getAllPinnedEventsByTimelineId', async () => { + expect((getAllPinnedEventsByTimelineId as jest.Mock).mock.calls[0][1]).toEqual( + mockSavedObject.id + ); + }); + + test('should retuen correct result', async () => { + expect(result).toEqual({ + totalCount: 1, + customTemplateTimelineCount: 0, + defaultTimelineCount: 0, + elasticTemplateTimelineCount: 1, + favoriteCount: 0, + templateTimelineCount: 1, + timeline: [ + { + ...mockGetTimelineValue, + noteIds: [], + pinnedEventIds: [], + eventIdToNoteIds: [], + favorite: [], + notes: [], + pinnedEventsSaveObject: [], + }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts index 6bc0ca64ae33..23ea3e621346 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts @@ -41,7 +41,7 @@ interface ResponseTimelines { totalCount: number; } -interface AllTimelinesResponse extends ResponseTimelines { +export interface AllTimelinesResponse extends ResponseTimelines { defaultTimelineCount: number; templateTimelineCount: number; elasticTemplateTimelineCount: number; @@ -63,7 +63,7 @@ export interface Timeline { getAllTimeline: ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -152,17 +152,18 @@ const getTimelineTypeFilter = ( export const getExistingPrepackagedTimelines = async ( request: FrameworkRequest, countsOnly?: boolean, - pageInfo?: PageInfoTimeline | null + pageInfo?: PageInfoTimeline ): Promise<{ totalCount: number; timeline: TimelineSavedObject[]; }> => { - const queryPageInfo = countsOnly - ? { - perPage: 1, - page: 1, - } - : pageInfo ?? {}; + const queryPageInfo = + countsOnly && pageInfo == null + ? { + perPage: 1, + page: 1, + } + : { perPage: pageInfo?.pageSize, page: pageInfo?.pageIndex } ?? {}; const elasticTemplateTimelineOptions = { type: timelineSavedObjectType, ...queryPageInfo, @@ -175,7 +176,7 @@ export const getExistingPrepackagedTimelines = async ( export const getAllTimeline = async ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -183,13 +184,13 @@ export const getAllTimeline = async ( ): Promise => { const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, - perPage: pageInfo?.pageSize ?? undefined, - page: pageInfo?.pageIndex ?? undefined, + perPage: pageInfo.pageSize, + page: pageInfo.pageIndex, search: search != null ? search : undefined, searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - filter: getTimelineTypeFilter(timelineType, status), + filter: getTimelineTypeFilter(timelineType ?? null, status ?? null), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; @@ -220,7 +221,7 @@ export const getAllTimeline = async ( searchFields: ['title', 'description', 'favorite.keySearch'], perPage: 1, page: 1, - filter: getTimelineTypeFilter(timelineType, TimelineStatus.active), + filter: getTimelineTypeFilter(timelineType ?? null, TimelineStatus.active), }; const result = await Promise.all([ @@ -496,7 +497,6 @@ const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObje ]); }) ); - return { totalCount: savedObjects.total, timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => @@ -532,14 +532,20 @@ export const timelineWithReduxProperties = ( pinnedEventsSaveObject: pinnedEvents, }); -export const getTimelines = async (request: FrameworkRequest, timelineIds?: string[] | null) => { +export const getSelectedTimelines = async ( + request: FrameworkRequest, + timelineIds?: string[] | null +) => { const savedObjectsClient = request.context.core.savedObjects.client; let exportedIds = timelineIds; if (timelineIds == null || timelineIds.length === 0) { const { timeline: savedAllTimelines } = await getAllTimeline( request, false, - null, + { + pageIndex: 1, + pageSize: timelineIds?.length ?? 0, + }, null, null, TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts index 90839f5ac01c..2a15f1fe074f 100644 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts @@ -131,7 +131,7 @@ describe('elasticsearch_adapter', () => { _id: 'id-9', _score: 0, _source: { - // @ts-ignore ts doesn't like seeing the object written this way, but sometimes this is the data we get! + // @ts-expect-error ts doesn't like seeing the object written this way, but sometimes this is the data we get! 'host.id': ['host-id-9'], 'host.name': ['host-9'], }, diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 37a97c03ad33..000bd875930f 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -37,7 +37,7 @@ import { cleanDraftTimelinesRoute } from '../lib/timeline/routes/clean_draft_tim import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/install_prepacked_timelines_route'; -import { getTimelineByIdRoute } from '../lib/timeline/routes/get_timeline_by_id_route'; +import { getTimelineRoute } from '../lib/timeline/routes/get_timeline_route'; export const initRoutes = ( router: IRouter, @@ -70,7 +70,7 @@ export const initRoutes = ( importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); getDraftTimelinesRoute(router, config, security); - getTimelineByIdRoute(router, config, security); + getTimelineRoute(router, config, security); cleanDraftTimelinesRoute(router, config, security); installPrepackedTimelinesRoute(router, config, security); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx index f8a1f8493732..f886b0b46148 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx @@ -20,7 +20,7 @@ export const RefreshTransformListButton: FC = ({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e2f59f3fa910..7e1ce610a194 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -698,6 +698,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "時間フィルターのデフォルト更新間隔「値」はミリ秒で指定する必要があります。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "タイムピッカーの更新間隔", "data.advancedSettings.timepicker.thisWeek": "今週", + "data.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", + "data.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", @@ -2791,8 +2793,6 @@ "kbn.advancedSettings.storeUrlTitle": "セッションストレージに URL を格納", "kbn.advancedSettings.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", "kbn.advancedSettings.themeVersionTitle": "テーマバージョン", - "kbn.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", - "kbn.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", @@ -8830,9 +8830,9 @@ "xpack.infra.metrics.alertFlyout.alertOnNoData": "データがない場合に通知する", "xpack.infra.metrics.alertFlyout.alertPreviewError": "このアラート条件をプレビューするときにエラーが発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "一部のデータを評価するときにエラーが発生しました。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} {groupName}{plural}", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} {groupName}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "すべてを対象にする", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データがない{were} {noData}結果{plural}がありました。", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データがない {noData}結果がありました。", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "このアラートは{firedTimes}回発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "過去{lookback}", "xpack.infra.metrics.alertFlyout.conditions": "条件", @@ -9799,7 +9799,6 @@ "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "値", "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "設定する値が必要です。", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}ドキュメント", - "xpack.ingestPipelines.pipelineEditor.testPipelineButtonLabel": "パイプラインをテスト", "xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError": "タイプが必要です。", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel": "プロセッサー", "xpack.ingestPipelines.processors.label.append": "末尾に追加", @@ -9857,13 +9856,11 @@ "xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink": "詳細", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText": "投入するパイプラインのドキュメントの配列を指定します。{learnMoreLink}", "xpack.ingestPipelines.testPipelineFlyout.executePipelineError": "パイプラインを実行できません", - "xpack.ingestPipelines.testPipelineFlyout.invalidPipelineErrorMessage": "実行するパイプラインが無効です。", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionLinkLabel": "出力を更新", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionText": "出力データを表示するか、パイプライン経由で渡されるときに各プロセッサーがドキュメントにどのように影響するのかを確認します。", "xpack.ingestPipelines.testPipelineFlyout.outputTab.verboseSwitchLabel": "冗長出力を表示", "xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "パイプラインが実行されました", "xpack.ingestPipelines.testPipelineFlyout.title": "パイプラインをテスト", - "xpack.ingestPipelines.testPipelineFlyout.withPipelineNameTitle": "パイプライン'{pipelineName}'をテスト", "xpack.lens.app.docLoadingError": "保存されたドキュメントの保存中にエラーが発生", "xpack.lens.app.docSavingError": "ドキュメントの保存中にエラーが発生", "xpack.lens.app.indexPatternLoadingError": "インデックスパターンの読み込み中にエラーが発生", @@ -14033,7 +14030,6 @@ "xpack.monitoring.setupMode.node": "ノード", "xpack.monitoring.setupMode.nodes": "ノード", "xpack.monitoring.setupMode.noMonitoringDataFound": "{product} {identifier} が検出されませんでした", - "xpack.monitoring.setupMode.notAvailableCloud": "この機能はクラウドで使用できません。", "xpack.monitoring.setupMode.notAvailablePermissions": "これを実行するために必要な権限がありません。", "xpack.monitoring.setupMode.notAvailableTitle": "設定モードは使用できません", "xpack.monitoring.setupMode.server": "サーバー", @@ -14321,7 +14317,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "その名前のリモートクラスターはありません。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "ES からレスポンスが返らず、クラスターを編集できません。", "xpack.reporting.breadcrumb": "レポート", - "xpack.reporting.browsers.chromium.chromiumClosed": "レポート生成時に Chromium の終了を検出しました。", "xpack.reporting.browsers.chromium.errorDetected": "レポート生成時にエラーを検出しました: {err}", "xpack.reporting.browsers.chromium.pageErrorDetected": "レポート生成時にページでエラーを検出しました: {err}", "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "無許可の着信 URL を受信しました: 「{interceptedUrl}」、終了します", @@ -19304,7 +19299,6 @@ "xpack.watcher.models.baseAction.simulateMessage": "アクション {id} のシミュレーションが完了しました", "xpack.watcher.models.baseAction.typeName": "アクション", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "不明なアクションタイプ {type} を作成しようとしました。", - "xpack.watcher.models.baseWatch.displayName": "新規ウォッチ", "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", "xpack.watcher.models.baseWatch.selectMessageText": "新規ウォッチをセットアップします。", "xpack.watcher.models.baseWatch.typeName": "ウォッチ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 316d3247d19d..e82ba7cc1d60 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -698,6 +698,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔。需要使用毫秒单位指定“值”。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间筛选刷新时间间隔", "data.advancedSettings.timepicker.thisWeek": "本周", + "data.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", + "data.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", @@ -2792,8 +2794,6 @@ "kbn.advancedSettings.storeUrlTitle": "将 URL 存储在会话存储中", "kbn.advancedSettings.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", "kbn.advancedSettings.themeVersionTitle": "主题版本", - "kbn.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", - "kbn.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", @@ -8832,9 +8832,9 @@ "xpack.infra.metrics.alertFlyout.alertOnNoData": "没数据时提醒我", "xpack.infra.metrics.alertFlyout.alertPreviewError": "尝试预览此告警条件时发生错误", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "尝试评估部分数据时发生错误。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} 个{groupName}{plural}", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} 个{groupName}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "在", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在{were} {noData} 个无数据结果{plural}。", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {noData} 个无数据结果。", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "此告警将发生 {firedTimes}", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback}。", "xpack.infra.metrics.alertFlyout.conditions": "条件", @@ -9801,7 +9801,6 @@ "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "值", "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "需要设置值。", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}文档", - "xpack.ingestPipelines.pipelineEditor.testPipelineButtonLabel": "测试管道", "xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError": "类型必填。", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel": "处理器", "xpack.ingestPipelines.processors.label.append": "追加", @@ -9859,13 +9858,11 @@ "xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink": "了解详情", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText": "为管道提供要采集的一系列文档。{learnMoreLink}", "xpack.ingestPipelines.testPipelineFlyout.executePipelineError": "无法执行管道", - "xpack.ingestPipelines.testPipelineFlyout.invalidPipelineErrorMessage": "要执行的管道无效。", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionLinkLabel": "刷新输出", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionText": "查看输出数据或了解文档通过管道时每个处理器对文档的影响。", "xpack.ingestPipelines.testPipelineFlyout.outputTab.verboseSwitchLabel": "查看详细输出", "xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "管道已执行", "xpack.ingestPipelines.testPipelineFlyout.title": "测试管道", - "xpack.ingestPipelines.testPipelineFlyout.withPipelineNameTitle": "测试管道“{pipelineName}”", "xpack.lens.app.docLoadingError": "加载已保存文档时出错", "xpack.lens.app.docSavingError": "保存文档时出错", "xpack.lens.app.indexPatternLoadingError": "加载索引模式时出错", @@ -14038,7 +14035,6 @@ "xpack.monitoring.setupMode.node": "节点", "xpack.monitoring.setupMode.nodes": "节点", "xpack.monitoring.setupMode.noMonitoringDataFound": "未检测到 {product} {identifier}", - "xpack.monitoring.setupMode.notAvailableCloud": "此功能在云上不可用。", "xpack.monitoring.setupMode.notAvailablePermissions": "您没有所需的权限来执行此功能。", "xpack.monitoring.setupMode.notAvailableTitle": "设置模式不可用", "xpack.monitoring.setupMode.server": "服务器", @@ -14326,7 +14322,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "没有该名称的远程集群。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "无法编辑集群,ES 未返回任何响应。", "xpack.reporting.breadcrumb": "报告", - "xpack.reporting.browsers.chromium.chromiumClosed": "Reporting 检测到 Chromium 已关闭。", "xpack.reporting.browsers.chromium.errorDetected": "Reporting 检测到错误:{err}", "xpack.reporting.browsers.chromium.pageErrorDetected": "Reporting 在页面上检测到错误:{err}", "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "接收到禁止的传出 URL:“{interceptedUrl}”,正在退出", @@ -19311,7 +19306,6 @@ "xpack.watcher.models.baseAction.simulateMessage": "已成功模拟操作 {id}", "xpack.watcher.models.baseAction.typeName": "操作", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "尝试创建的操作类型 {type} 未知。", - "xpack.watcher.models.baseWatch.displayName": "新建监视", "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 参数必须包含 {id} 属性", "xpack.watcher.models.baseWatch.selectMessageText": "设置新监视。", "xpack.watcher.models.baseWatch.typeName": "监视", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 15099242b6e1..eb6b1ada3ba9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -22,7 +22,7 @@ import { import { i18n } from '@kbn/i18n'; import { Section, routeToConnectors, routeToAlerts } from './constants'; -import { getCurrentBreadcrumb } from './lib/breadcrumb'; +import { getAlertingSectionBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; import { useAppDependencies } from './app_context'; import { hasShowActionsCapability } from './lib/capabilities'; @@ -75,7 +75,7 @@ export const TriggersActionsUIHome: React.FunctionComponent { - setBreadcrumbs([getCurrentBreadcrumb(section || 'home')]); + setBreadcrumbs([getAlertingSectionBreadcrumb(section || 'home')]); chrome.docTitle.change(getCurrentDocTitle(section || 'home')); }, [section, chrome, setBreadcrumbs]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts index 8ba909beff2a..f5578aa5271b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts @@ -3,25 +3,25 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getCurrentBreadcrumb } from './breadcrumb'; +import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from './breadcrumb'; import { i18n } from '@kbn/i18n'; import { routeToConnectors, routeToAlerts, routeToHome } from '../constants'; -describe('getCurrentBreadcrumb', () => { +describe('getAlertingSectionBreadcrumb', () => { test('if change calls return proper breadcrumb title ', async () => { - expect(getCurrentBreadcrumb('connectors')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('connectors')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', { defaultMessage: 'Connectors', }), href: `${routeToConnectors}`, }); - expect(getCurrentBreadcrumb('alerts')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('alerts')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.alerts.breadcrumbTitle', { defaultMessage: 'Alerts', }), href: `${routeToAlerts}`, }); - expect(getCurrentBreadcrumb('home')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('home')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', { defaultMessage: 'Alerts and Actions', }), @@ -29,3 +29,14 @@ describe('getCurrentBreadcrumb', () => { }); }); }); + +describe('getAlertDetailsBreadcrumb', () => { + test('if select an alert should return proper breadcrumb title with alert name ', async () => { + expect(getAlertDetailsBreadcrumb('testId', 'testName')).toMatchObject({ + text: i18n.translate('xpack.triggersActionsUI.alertDetails.breadcrumbTitle', { + defaultMessage: 'testName', + }), + href: '/alert/testId', + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts index 3735942ff97a..db624688f9c7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { routeToHome, routeToConnectors, routeToAlerts } from '../constants'; +import { routeToHome, routeToConnectors, routeToAlerts, routeToAlertDetails } from '../constants'; -export const getCurrentBreadcrumb = (type: string): { text: string; href: string } => { +export const getAlertingSectionBreadcrumb = (type: string): { text: string; href: string } => { // Home and sections switch (type) { case 'connectors': @@ -33,3 +33,13 @@ export const getCurrentBreadcrumb = (type: string): { text: string; href: string }; } }; + +export const getAlertDetailsBreadcrumb = ( + id: string, + name: string +): { text: string; href: string } => { + return { + text: name, + href: `${routeToAlertDetails.replace(':alertId', id)}`, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index b1dd78ff59f3..6ee7915e2be7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useEffect } from 'react'; import { keyBy } from 'lodash'; import { useHistory } from 'react-router-dom'; import { @@ -29,6 +29,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; +import { getCurrentDocTitle } from '../../../lib/doc_title'; import { Alert, AlertType, ActionType } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -69,8 +71,20 @@ export const AlertDetails: React.FunctionComponent = ({ docLinks, charts, dataPlugin, + setBreadcrumbs, + chrome, } = useAppDependencies(); + // Set breadcrumb and page title + useEffect(() => { + setBreadcrumbs([ + getAlertingSectionBreadcrumb('alerts'), + getAlertDetailsBreadcrumb(alert.id, alert.name), + ]); + chrome.docTitle.change(getCurrentDocTitle('alerts')); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const canExecuteActions = hasExecuteActionsCapability(capabilities); const canSaveAlert = hasAllPrivilege(alert, alertType) && diff --git a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js index eaced3e27c8a..6b7d693bb308 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js @@ -79,11 +79,7 @@ export class BaseWatch { }; get displayName() { - if (this.isNew) { - return i18n.translate('xpack.watcher.models.baseWatch.displayName', { - defaultMessage: 'New Watch', - }); - } else if (this.name) { + if (this.name) { return this.name; } else { return this.id; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts index 2d62bca75c1a..36dfdb55b4ab 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts @@ -66,12 +66,19 @@ export async function saveWatch(watch: BaseWatch, toasts: ToastsSetup): Promise< try { await createWatch(watch); toasts.addSuccess( - i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { - defaultMessage: "Saved '{watchDisplayName}'", - values: { - watchDisplayName: watch.displayName, - }, - }) + watch.isNew + ? i18n.translate('xpack.watcher.sections.watchEdit.json.createSuccessNotificationText', { + defaultMessage: "Created '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) + : i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { + defaultMessage: "Saved '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) ); goToWatchList(); } catch (error) { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index fd0d03dc1841..7c43ac0bbe56 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -5,16 +5,14 @@ */ import { CoreSetup } from 'src/core/server'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; -import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server'; +import { ActionType } from '../../../../../../../plugins/actions/server'; export function defineActionTypes( core: CoreSetup, { actions }: Pick ) { - const clusterClient = core.elasticsearch.legacy.client; - // Action types const noopActionType: ActionType = { id: 'test.noop', @@ -32,24 +30,39 @@ export function defineActionTypes( throw new Error('this action is intended to fail'); }, }; - const indexRecordActionType: ActionType = { + actions.registerType(noopActionType); + actions.registerType(throwActionType); + actions.registerType(getIndexRecordActionType()); + actions.registerType(getFailingActionType()); + actions.registerType(getRateLimitedActionType()); + actions.registerType(getAuthorizationActionType(core)); +} + +function getIndexRecordActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + message: schema.string(), + }); + type ParamsType = TypeOf; + const configSchema = schema.object({ + unencrypted: schema.string(), + }); + type ConfigType = TypeOf; + const secretsSchema = schema.object({ + encrypted: schema.string(), + }); + type SecretsType = TypeOf; + const result: ActionType = { id: 'test.index-record', name: 'Test: Index Record', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - message: schema.string(), - }), - config: schema.object({ - unencrypted: schema.string(), - }), - secrets: schema.object({ - encrypted: schema.string(), - }), + params: paramsSchema, + config: configSchema, + secrets: secretsSchema, }, - async executor({ config, secrets, params, services, actionId }: ActionTypeExecutorOptions) { + async executor({ config, secrets, params, services, actionId }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -64,17 +77,23 @@ export function defineActionTypes( return { status: 'ok', actionId }; }, }; - const failingActionType: ActionType = { + return result; +} + +function getFailingActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.failing', name: 'Test: Failing', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - }), + params: paramsSchema, }, - async executor({ config, secrets, params, services }: ActionTypeExecutorOptions) { + async executor({ config, secrets, params, services }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -89,19 +108,25 @@ export function defineActionTypes( throw new Error(`expected failure for ${params.index} ${params.reference}`); }, }; - const rateLimitedActionType: ActionType = { + return result; +} + +function getRateLimitedActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + retryAt: schema.number(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.rate-limit', name: 'Test: Rate Limit', minimumLicenseRequired: 'gold', maxAttempts: 2, validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - retryAt: schema.number(), - }), + params: paramsSchema, }, - async executor({ config, params, services }: ActionTypeExecutorOptions) { + async executor({ config, params, services }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -119,20 +144,27 @@ export function defineActionTypes( }; }, }; - const authorizationActionType: ActionType = { + return result; +} + +function getAuthorizationActionType(core: CoreSetup) { + const clusterClient = core.elasticsearch.legacy.client; + const paramsSchema = schema.object({ + callClusterAuthorizationIndex: schema.string(), + savedObjectsClientType: schema.string(), + savedObjectsClientId: schema.string(), + index: schema.string(), + reference: schema.string(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.authorization', name: 'Test: Authorization', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - callClusterAuthorizationIndex: schema.string(), - savedObjectsClientType: schema.string(), - savedObjectsClientId: schema.string(), - index: schema.string(), - reference: schema.string(), - }), + params: paramsSchema, }, - async executor({ params, services, actionId }: ActionTypeExecutorOptions) { + async executor({ params, services, actionId }) { // Call cluster let callClusterSuccess = false; let callClusterError; @@ -200,10 +232,5 @@ export function defineActionTypes( }; }, }; - actions.registerType(noopActionType); - actions.registerType(throwActionType); - actions.registerType(indexRecordActionType); - actions.registerType(failingActionType); - actions.registerType(rateLimitedActionType); - actions.registerType(authorizationActionType); + return result; } diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 553cceef573e..0c4097a1d5c4 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -11,6 +11,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'apiKeys']); const log = getService('log'); const security = getService('security'); + const testSubjects = getService('testSubjects'); describe('Home page', function () { before(async () => { @@ -32,10 +33,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('Loads the app', async () => { await security.testUser.setRoles(['test_api_keys']); log.debug('Checking for section header'); - const headerText = await pageObjects.apiKeys.noAPIKeysHeading(); - expect(headerText).to.be('No API keys'); - const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton(); - expect(await goToConsoleButton.isDisplayed()).to.be(true); + const headers = await testSubjects.findAll('noApiKeysHeader'); + if (headers.length > 0) { + expect(await headers[0].getVisibleText()).to.be('No API keys'); + const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton(); + expect(await goToConsoleButton.isDisplayed()).to.be(true); + } else { + // page may already contain EiTable with data, then check API Key Admin text + const description = await pageObjects.apiKeys.getApiKeyAdminDesc(); + expect(description).to.be('You are an API Key administrator.'); + } }); }); }; diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index 20ad045d0a65..33db56751285 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,8 +19,7 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - // FLAKY: https://github.com/elastic/kibana/issues/63339 - describe.skip('custom elements', function () { + describe('custom elements', function () { this.tags('skipFirefox'); before(async () => { @@ -66,6 +65,7 @@ export default function canvasCustomElementTest({ // ensure the custom element is the one expected and click it to add to the workpad const customElement = await find.byCssSelector('.canvasElementCard__wrapper'); const elementName = await customElement.findByCssSelector('.euiCard__title'); + expect(await elementName.getVisibleText()).to.contain('My New Element'); customElement.click(); diff --git a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js index d01883e9ea54..3564c72cfc34 100644 --- a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js +++ b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js @@ -8,11 +8,10 @@ export const getLifecycleMethods = (getService, getPageObjects) => { const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['monitoring', 'timePicker', 'security']); - const noData = getService('monitoringNoData'); let _archive; return { - async setup(archive, { from, to }) { + async setup(archive, { from, to, useSuperUser = false }) { _archive = archive; const kibanaServer = getService('kibanaServer'); @@ -24,8 +23,7 @@ export const getLifecycleMethods = (getService, getPageObjects) => { await esArchiver.load(archive); await kibanaServer.uiSettings.replace({}); - await PageObjects.monitoring.navigateTo(); - await noData.isOnNoDataPage(); + await PageObjects.monitoring.navigateTo(useSuperUser); // pause autorefresh in the time filter because we don't wait any ticks, // and we don't want ES to log a warning when data gets wiped out diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index c383d8593a4f..6a2037bbc492 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -39,5 +39,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./time_filter')); loadTestFile(require.resolve('./enable_monitoring')); + + loadTestFile(require.resolve('./setup/metricbeat_migration')); }); } diff --git a/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js new file mode 100644 index 000000000000..95bd866d386b --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js @@ -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 expect from '@kbn/expect'; +import { getLifecycleMethods } from '../_get_lifecycle_methods'; + +export default function ({ getService, getPageObjects }) { + const setupMode = getService('monitoringSetupMode'); + const PageObjects = getPageObjects(['common', 'console']); + + // FLAKY: https://github.com/elastic/kibana/issues/74327 + describe.skip('Setup mode metricbeat migration', function () { + describe('setup mode btn', () => { + const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + + before(async () => { + await setup('monitoring/setup/collection/es_and_kibana_mb', { + from: 'Apr 9, 2019 @ 00:00:00.741', + to: 'Apr 9, 2019 @ 23:59:59.741', + useSuperUser: true, + }); + }); + + after(async () => { + await tearDown(); + }); + + it('should exist', async () => { + expect(await setupMode.doesSetupModeBtnAppear()).to.be(true); + }); + + it('should be clickable and show the bottom bar', async () => { + await setupMode.clickSetupModeBtn(); + await PageObjects.common.sleep(1000); // bottom drawer animation + expect(await setupMode.doesBottomBarAppear()).to.be(true); + }); + + it('should not show metricbeat migration if cloud', async () => { + const isCloud = await PageObjects.common.isCloud(); + expect(await setupMode.doesMetricbeatMigrationTooltipAppear()).to.be(!isCloud); + }); + + // TODO: this does not work because TLS isn't enabled in the test env + // it('should show alerts all the time', async () => { + // expect(await setupMode.doesAlertsTooltipAppear()).to.be(true); + // }); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz new file mode 100644 index 000000000000..c434eee5dd8d Binary files /dev/null and b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json new file mode 100644 index 000000000000..1432a53b4546 --- /dev/null +++ b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json @@ -0,0 +1,2216 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "67c28185da541c1404e7852d30498cd6", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "4818e7dfc3e538562c80ec34eb6f841b", + "exception-list-agnostic": "4818e7dfc3e538562c80ec34eb6f841b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "ingest-agent-configs": "9326f99c977fd2ef5ab24b6336a0675c", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-configs": "48e8bd97e488008e21c0b5a2367b83ad", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "ingest-agent-configs": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-configs": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "dataset": { + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "canvas-workpad": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/api_keys_page.ts b/x-pack/test/functional/page_objects/api_keys_page.ts index 17f4df74921b..fa10c5a574c0 100644 --- a/x-pack/test/functional/page_objects/api_keys_page.ts +++ b/x-pack/test/functional/page_objects/api_keys_page.ts @@ -14,6 +14,10 @@ export function ApiKeysPageProvider({ getService }: FtrProviderContext) { return await testSubjects.getVisibleText('noApiKeysHeader'); }, + async getApiKeyAdminDesc() { + return await testSubjects.getVisibleText('apiKeyAdminDescriptionCallOut'); + }, + async getGoToConsoleButton() { return await testSubjects.find('goToConsoleButton'); }, diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index f08d1e6b7fef..23b5057573b3 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -8,10 +8,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; -export function CanvasPageProvider({ getService }: FtrProviderContext) { +export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); return { async enterFullscreen() { @@ -58,6 +59,8 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { async openSavedElementsModal() { await testSubjects.click('add-element-button'); await testSubjects.click('saved-elements-menu-option'); + + await PageObjects.common.sleep(1000); // give time for modal animation to complete }, async closeSavedElementsModal() { await testSubjects.click('saved-elements-modal-close-button'); diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 4b6342758be9..6d8eade25d7e 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -30,6 +30,7 @@ import { MonitoringKibanaInstancesProvider, MonitoringKibanaInstanceProvider, MonitoringKibanaSummaryStatusProvider, + MonitoringSetupModeProvider, // @ts-ignore not ts yet } from './monitoring'; // @ts-ignore not ts yet @@ -85,6 +86,7 @@ export const services = { monitoringKibanaInstances: MonitoringKibanaInstancesProvider, monitoringKibanaInstance: MonitoringKibanaInstanceProvider, monitoringKibanaSummaryStatus: MonitoringKibanaSummaryStatusProvider, + monitoringSetupMode: MonitoringSetupModeProvider, pipelineList: PipelineListProvider, pipelineEditor: PipelineEditorProvider, random: RandomProvider, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index f452c9cce7a1..d315f9eb7721 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -62,8 +62,15 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return rows; } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlAnalyticsRefreshListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlAnalyticsRefreshListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshAnalyticsTable() { - await testSubjects.click('mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForAnalyticsToLoad(); } diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index a72d9c204060..58a1afad88e1 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -141,8 +141,15 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlRefreshJobListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlRefreshJobListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshJobList() { - await testSubjects.click('mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForJobsToLoad(); } diff --git a/x-pack/test/functional/services/monitoring/index.js b/x-pack/test/functional/services/monitoring/index.js index 0087cbaae2b0..0187a3f97683 100644 --- a/x-pack/test/functional/services/monitoring/index.js +++ b/x-pack/test/functional/services/monitoring/index.js @@ -25,3 +25,4 @@ export { MonitoringKibanaOverviewProvider } from './kibana_overview'; export { MonitoringKibanaInstancesProvider } from './kibana_instances'; export { MonitoringKibanaInstanceProvider } from './kibana_instance'; export { MonitoringKibanaSummaryStatusProvider } from './kibana_summary_status'; +export { MonitoringSetupModeProvider } from './setup_mode'; diff --git a/x-pack/test/functional/services/monitoring/setup_mode.js b/x-pack/test/functional/services/monitoring/setup_mode.js new file mode 100644 index 000000000000..a71ad924a852 --- /dev/null +++ b/x-pack/test/functional/services/monitoring/setup_mode.js @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function MonitoringSetupModeProvider({ getService }) { + const testSubjects = getService('testSubjects'); + + const SUBJ_SETUP_MODE_BTN = 'monitoringSetupModeBtn'; + const SUBJ_SETUP_MODE_BOTTOM_BAR = 'monitoringSetupModeBottomBar'; + const SUBJ_SETUP_MODE_METRICBEAT_MIGRATION_TOOLTIP = + 'monitoringSetupModeMetricbeatMigrationTooltip'; + const SUBJ_SETUP_MODE_ALERTS_BADGE = 'monitoringSetupModeAlertBadges'; + + return new (class SetupMode { + async doesSetupModeBtnAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_BTN); + } + + async clickSetupModeBtn() { + return await testSubjects.click(SUBJ_SETUP_MODE_BTN); + } + + async doesBottomBarAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_BOTTOM_BAR); + } + + async doesMetricbeatMigrationTooltipAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_METRICBEAT_MIGRATION_TOOLTIP); + } + + async doesAlertsTooltipAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_ALERTS_BADGE); + } + })(); +} diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 453dca904b60..37d8b6e51072 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -95,8 +95,19 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~transformRefreshTransformListButton', { + timeout: 10 * 1000, + }); + await testSubjects.existOrFail('transformRefreshTransformListButton loaded', { + timeout: 30 * 1000, + }); + } + public async refreshTransformList() { - await testSubjects.click('transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForTransformsToLoad(); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js new file mode 100644 index 000000000000..1582f72dd1cd --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.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. + */ + +export default function loadTests({ loadTestFile }) { + describe('EPM Endpoints', () => { + loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./file')); + //loadTestFile(require.resolve('./template')); + loadTestFile(require.resolve('./ilm')); + loadTestFile(require.resolve('./install_overrides')); + loadTestFile(require.resolve('./install_remove_assets')); + loadTestFile(require.resolve('./install_update')); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 9ca8ebf13607..35058de0684b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -108,6 +108,54 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSearch.id).equal('sample_search'); }); + it('should have created the correct saved object', async function () { + const res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + expect(res.attributes).eql({ + installed_kibana: [ + { + id: 'sample_dashboard', + type: 'dashboard', + }, + { + id: 'sample_dashboard2', + type: 'dashboard', + }, + { + id: 'sample_search', + type: 'search', + }, + { + id: 'sample_visualization', + type: 'visualization', + }, + ], + installed_es: [ + { + id: 'logs-all_assets.test_logs-0.1.0', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs', + type: 'index_template', + }, + { + id: 'metrics-all_assets.test_metrics', + type: 'index_template', + }, + ], + es_index_patterns: { + test_logs: 'logs-all_assets.test_logs-*', + test_metrics: 'metrics-all_assets.test_metrics-*', + }, + name: 'all_assets', + version: '0.1.0', + internal: false, + removable: true, + }); + }); }); describe('uninstalls all assets when uninstalling a package', async () => { @@ -192,6 +240,18 @@ export default function (providerContext: FtrProviderContext) { } expect(resSearch.response.data.statusCode).equal(404); }); + it('should have removed the saved object', async function () { + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts new file mode 100644 index 000000000000..9de6cd9118fe --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('installing and updating scenarios', async () => { + skipIfNoDockerRegistry(providerContext); + after(async () => { + await deletePackage('multiple_versions-0.3.0'); + }); + + it('should return 404 if package does not exist', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(404); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'nonexistent', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 400 if trying to install an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'update', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 200 if trying to force install an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('should return 400 if trying to update to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + it('should return 200 if trying to force update to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('should return 200 if trying to update to the latest package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.3.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + await deletePackage('multiple_versions-0.3.0'); + }); + it('should return 200 if trying to install the latest package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.3.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index 98b26c1c04eb..20414fcb9052 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(13); + expect(listResponse.response.length).to.be(14); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/fields/fields.yml new file mode 100644 index 000000000000..12a9a03c1337 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/manifest.yml new file mode 100644 index 000000000000..9ac3c68a0be9 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/docs/README.md new file mode 100644 index 000000000000..13ef3f4fa915 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml new file mode 100644 index 000000000000..32c626b11573 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a package +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/fields/fields.yml new file mode 100644 index 000000000000..12a9a03c1337 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/manifest.yml new file mode 100644 index 000000000000..9ac3c68a0be9 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/docs/README.md new file mode 100644 index 000000000000..8e26522d8683 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml new file mode 100644 index 000000000000..773903a69e7f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a packagee +version: 0.2.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml new file mode 100644 index 000000000000..12a9a03c1337 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml new file mode 100644 index 000000000000..9ac3c68a0be9 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md new file mode 100644 index 000000000000..8e26522d8683 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml new file mode 100644 index 000000000000..49c85994d2c2 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a package +version: 0.3.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index d21b80bd6eed..72121b2164bf 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -12,12 +12,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./fleet/index')); // EPM - loadTestFile(require.resolve('./epm/list')); - loadTestFile(require.resolve('./epm/file')); - //loadTestFile(require.resolve('./epm/template')); - loadTestFile(require.resolve('./epm/ilm')); - loadTestFile(require.resolve('./epm/install_overrides')); - loadTestFile(require.resolve('./epm/install_remove_assets')); + loadTestFile(require.resolve('./epm/index')); // Package configs loadTestFile(require.resolve('./package_config/create')); diff --git a/x-pack/test/reporting_api_integration/config.js b/x-pack/test/reporting_api_integration/config.js index 3a3e72e5bc23..930c90da5637 100644 --- a/x-pack/test/reporting_api_integration/config.js +++ b/x-pack/test/reporting_api_integration/config.js @@ -12,6 +12,14 @@ export default async function ({ readConfigFile }) { const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Reporting API tests need a fully working UI + const testPolicyRules = [ + { allow: true, protocol: 'http:' }, + { allow: false, host: 'via.placeholder.com' }, + { allow: true, protocol: 'https:' }, + { allow: true, protocol: 'data:' }, + { allow: false }, + ]; + return { servers: apiConfig.get('servers'), junit: { reportName: 'X-Pack Reporting API Integration Tests' }, @@ -36,6 +44,7 @@ export default async function ({ readConfigFile }) { `--xpack.reporting.queue.pollInterval=3000`, `--xpack.security.session.idleTimeout=3600000`, `--xpack.spaces.enabled=false`, + `--xpack.reporting.capture.networkPolicy.rules=${JSON.stringify(testPolicyRules)}`, ], }, esArchiver: apiConfig.get('esArchiver'), diff --git a/x-pack/test/reporting_api_integration/reporting/index.ts b/x-pack/test/reporting_api_integration/reporting/index.ts index 4b517f0b970c..18ef28150c42 100644 --- a/x-pack/test/reporting_api_integration/reporting/index.ts +++ b/x-pack/test/reporting_api_integration/reporting/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./csv_job_params')); loadTestFile(require.resolve('./csv_saved_search')); loadTestFile(require.resolve('./usage')); + loadTestFile(require.resolve('./network_policy')); }); } diff --git a/x-pack/test/reporting_api_integration/reporting/network_policy.ts b/x-pack/test/reporting_api_integration/reporting/network_policy.ts new file mode 100644 index 000000000000..9f9800cafb99 --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting/network_policy.ts @@ -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 expect from '@kbn/expect'; +import * as Rx from 'rxjs'; +import { filter, first, map, switchMap, timeout } from 'rxjs/operators'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const reportingAPI = getService('reportingAPI'); + const supertest = getService('supertest'); + const archive = 'reporting/canvas_disallowed_url'; + + /* + * The Reporting API Functional Test config implements a network policy that + * is designed to disallow the following Canvas worksheet + */ + describe('reporting network policy', () => { + before(async () => { + await esArchiver.load(archive); // includes a canvas worksheet with an offending image URL + }); + + after(async () => { + await esArchiver.unload(archive); + }); + + it('should fail job when page voilates the network policy', async () => { + const downloadPath = await reportingAPI.postJob( + `/api/reporting/generate/printablePdf?jobParams=(layout:(dimensions:(height:720,width:1080),id:preserve_layout),objectType:'canvas%20workpad',relativeUrls:!(%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fworkpad-e7464259-0b75-4b8c-81c8-8422b15ff201%2Fpage%2F1),title:'My%20Canvas%20Workpad')` + ); + + // Retry the download URL until a "failed" response status is returned + const fails$: Rx.Observable = Rx.interval(100).pipe( + switchMap(() => supertest.get(downloadPath).then((response) => response.body)), + filter(({ statusCode }) => statusCode === 500), + map(({ message }) => message), + first(), + timeout(15000) + ); + + const reportFailed = await fails$.toPromise(); + + expect(reportFailed).to.match(/Reporting generation failed: Error:/); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 9cdef1c93889..8b4a73b7eb84 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -26,8 +26,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider before(async () => { await ingestManager.setup(); }); - loadTestFile(require.resolve('./resolver/entity_id')); - loadTestFile(require.resolve('./resolver/tree')); + loadTestFile(require.resolve('./resolver/index')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 719327e5f9b7..3afa9f397a2e 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -119,7 +119,11 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') - .send({ filter: 'not host.ip:10.46.229.234' }) + .send({ + filters: { + kql: 'not host.ip:10.46.229.234', + }, + }) .expect(200); expect(body.total).to.eql(2); expect(body.hosts.length).to.eql(2); @@ -141,7 +145,9 @@ export default function ({ getService }: FtrProviderContext) { page_index: 0, }, ], - filter: `not host.ip:${notIncludedIp}`, + filters: { + kql: `not host.ip:${notIncludedIp}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -166,7 +172,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.os.Ext.variant:${variantValue}`, + filters: { + kql: `host.os.Ext.variant:${variantValue}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -185,7 +193,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.ip:${targetEndpointIp}`, + filters: { + kql: `host.ip:${targetEndpointIp}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -204,7 +214,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `not Endpoint.policy.applied.status:success`, + filters: { + kql: `not Endpoint.policy.applied.status:success`, + }, }) .expect(200); const statuses: Set = new Set( @@ -223,7 +235,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `elastic.agent.id:${targetElasticAgentId}`, + filters: { + kql: `elastic.agent.id:${targetElasticAgentId}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -243,7 +257,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: '', + filters: { + kql: '', + }, }) .expect(200); expect(body.total).to.eql(numberOfHostsInFixture); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts new file mode 100644 index 000000000000..cde1a3616b62 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { entityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; +import { PaginationBuilder } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/utils/pagination'; +import { ChildrenQuery } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/queries/children'; +import { + ResolverTree, + ResolverEvent, +} from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + Event, + EndpointDocGenerator, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { InsertedEvents } from '../../services/resolver'; + +export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const resolver = getService('resolverGenerator'); + const generator = new EndpointDocGenerator('resolver'); + const es = getService('es'); + + describe('Resolver children edge cases', () => { + describe('info and exec children', () => { + let origin: Event; + let infoEvent: Event; + let startEvent: Event; + let execEvent: Event; + let genData: InsertedEvents; + + before(async () => { + // Construct the following tree: + // Origin -> infoEvent -> startEvent -> execEvent + origin = generator.generateEvent(); + infoEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['info'], + }); + + startEvent = generator.generateEvent({ + parentEntityID: infoEvent.process.entity_id, + ancestry: [infoEvent.process.entity_id, origin.process.entity_id], + eventType: ['start'], + }); + + execEvent = generator.generateEvent({ + parentEntityID: startEvent.process.entity_id, + ancestry: [startEvent.process.entity_id, infoEvent.process.entity_id], + eventType: ['change'], + }); + genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('finds all the children of the origin', async () => { + const { body }: { body: ResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process.entity_id}?children=100`) + .expect(200); + expect(body.children.childNodes.length).to.be(3); + expect(body.children.childNodes[0].entityID).to.be(infoEvent.process.entity_id); + expect(body.children.childNodes[1].entityID).to.be(startEvent.process.entity_id); + expect(body.children.childNodes[2].entityID).to.be(execEvent.process.entity_id); + }); + }); + + describe('duplicate process running events', () => { + let origin: Event; + let startEvent: Event; + let infoEvent: Event; + let execEvent: Event; + let genData: InsertedEvents; + + before(async () => { + // Construct the following tree: + // Origin -> (infoEvent, startEvent, execEvent are all for the same node) + origin = generator.generateEvent(); + startEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['start'], + }); + + infoEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + entityID: startEvent.process.entity_id, + eventType: ['info'], + }); + + execEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['change'], + entityID: startEvent.process.entity_id, + }); + genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('only retrieves the start event for the child node', async () => { + const childrenQuery = new ChildrenQuery( + PaginationBuilder.createBuilder(100), + eventsIndexPattern + ); + // [1] here gets the body portion of the array + const [, query] = childrenQuery.buildMSearch(origin.process.entity_id); + const { body } = await es.search>({ body: query }); + expect(body.hits.hits.length).to.be(1); + + const event = body.hits.hits[0]._source; + expect(entityId(event)).to.be(startEvent.process.entity_id); + expect(event.event?.type).to.eql(['start']); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index 231871fae3d3..cb6c49e17c71 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -16,7 +16,7 @@ import { } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { InsertedEvents } from '../../services/resolver'; -export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); const generator = new EndpointDocGenerator('resolver'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts new file mode 100644 index 000000000000..dc9a1fab9ec0 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { loadTestFile } = providerContext; + + describe('Resolver tests', () => { + loadTestFile(require.resolve('./entity_id')); + loadTestFile(require.resolve('./children')); + loadTestFile(require.resolve('./tree')); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 3527e7e575c9..7b511c3be74b 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -230,7 +230,7 @@ const verifyLifecycleStats = ( } }; -export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const resolver = getService('resolverGenerator'); @@ -566,7 +566,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ).to.eql(93932); }); - it('returns no values when there is no more data', async () => { + // The children api does not support pagination currently + it.skip('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( @@ -577,7 +578,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.eql(null); }); - it('returns the first page of information when the cursor is invalid', async () => { + // The children api does not support pagination currently + it.skip('returns the first page of information when the cursor is invalid', async () => { const { body }: { body: ResolverChildren } = await supertest .get( `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` @@ -639,7 +641,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.not.eql(null); }); - it('paginates the children', async () => { + // children api does not support pagination currently + it.skip('paginates the children', async () => { // this gets a node should have 3 children which were created in succession so that the timestamps // are ordered correctly to be retrieved in a single call const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; @@ -668,7 +671,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.be(null); }); - it('gets all children in two queries', async () => { + // children api does not support pagination currently + it.skip('gets all children in two queries', async () => { // should get all the children of the origin let { body }: { body: ResolverChildren } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=3`) diff --git a/yarn.lock b/yarn.lock index a70f04e03044..0638267afd25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4527,11 +4527,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.44": - version "0.0.44" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21" - integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g== - "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -6188,7 +6183,7 @@ acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== -acorn-walk@^7.0.0, acorn-walk@^7.1.1: +acorn-walk@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== @@ -6218,7 +6213,7 @@ acorn@^7.0.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ== -acorn@^7.1.0, acorn@^7.1.1: +acorn@^7.1.0: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==