diff --git a/.ci/Dockerfile b/.ci/Dockerfile new file mode 100644 index 0000000000000..d90d9f4710b5b --- /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 b0d3591821642..9a49c19b94df2 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/pipeline-library/src/test/slackNotifications.groovy b/.ci/pipeline-library/src/test/slackNotifications.groovy index f7e39f5fad903..33b3afed80bde 100644 --- a/.ci/pipeline-library/src/test/slackNotifications.groovy +++ b/.ci/pipeline-library/src/test/slackNotifications.groovy @@ -9,6 +9,7 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { super.setUp() helper.registerAllowedMethod('slackSend', [Map.class], null) + prop('buildState', loadScript("vars/buildState.groovy")) slackNotifications = loadScript('vars/slackNotifications.groovy') } @@ -25,13 +26,49 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { } @Test - void 'sendFailedBuild() should call slackSend() with message'() { + void 'sendFailedBuild() should call slackSend() with an in-progress message'() { mockFailureBuild() slackNotifications.sendFailedBuild() def args = fnMock('slackSend').args[0] + def expected = [ + channel: '#kibana-operations-alerts', + username: 'Kibana Operations', + iconEmoji: ':jenkins:', + color: 'danger', + message: ':hourglass_flowing_sand: elastic / kibana # master #1', + ] + + expected.each { + assertEquals(it.value.toString(), args[it.key].toString()) + } + + assertEquals( + ":hourglass_flowing_sand: **", + args.blocks[0].text.text.toString() + ) + + assertEquals( + "*Failed Steps*\n• ", + args.blocks[1].text.text.toString() + ) + + assertEquals( + "*Test Failures*\n• ", + args.blocks[2].text.text.toString() + ) + } + + @Test + void 'sendFailedBuild() should call slackSend() with message'() { + mockFailureBuild() + + slackNotifications.sendFailedBuild(isFinal: true) + + def args = fnMock('slackSend').args[0] + def expected = [ channel: '#kibana-operations-alerts', username: 'Kibana Operations', @@ -65,7 +102,7 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { mockFailureBuild() def counter = 0 helper.registerAllowedMethod('slackSend', [Map.class], { ++counter > 1 }) - slackNotifications.sendFailedBuild() + slackNotifications.sendFailedBuild(isFinal: true) def args = fnMocks('slackSend')[1].args[0] @@ -88,6 +125,29 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { ) } + @Test + void 'sendFailedBuild() should call slackSend() with a channel id and timestamp on second call'() { + mockFailureBuild() + helper.registerAllowedMethod('slackSend', [Map.class], { [ channelId: 'CHANNEL_ID', ts: 'TIMESTAMP' ] }) + slackNotifications.sendFailedBuild(isFinal: false) + slackNotifications.sendFailedBuild(isFinal: true) + + def args = fnMocks('slackSend')[1].args[0] + + def expected = [ + channel: 'CHANNEL_ID', + timestamp: 'TIMESTAMP', + username: 'Kibana Operations', + iconEmoji: ':jenkins:', + color: 'danger', + message: ':broken_heart: elastic / kibana # master #1', + ] + + expected.each { + assertEquals(it.value.toString(), args[it.key].toString()) + } + } + @Test void 'getTestFailures() should truncate list of failures to 10'() { prop('testUtils', [ diff --git a/.ci/runbld_no_junit.yml b/.ci/runbld_no_junit.yml index 67b5002c1c437..1bcb7e22a2648 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/.gitignore b/.gitignore index dfd02de7b1186..1d12ef2a9cff3 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 818ba748ee165..3b68cde206573 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,59 +4,18 @@ library 'kibana-pipeline-library' kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) { - githubPr.withDefaultPrComments { - 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'), - ]), - ]) + slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { + githubPr.withDefaultPrComments { + ciStats.trackBuild { + catchError { + retryable.enable() + kibanaPipeline.allCiTasks() + } } } } if (params.NOTIFY_ON_FAILURE) { - slackNotifications.onFailure() kibanaPipeline.sendMail() } } diff --git a/docs/apm/images/apm-anomaly-alert.png b/docs/apm/images/apm-anomaly-alert.png new file mode 100644 index 0000000000000..35ce9a2296c9c Binary files /dev/null and b/docs/apm/images/apm-anomaly-alert.png differ diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc index b203b8668072f..db2a1ef6e2da0 100644 --- a/docs/apm/machine-learning.asciidoc +++ b/docs/apm/machine-learning.asciidoc @@ -1,36 +1,61 @@ [role="xpack"] [[machine-learning-integration]] -=== integration +=== Machine learning integration ++++ Integrate with machine learning ++++ -The Machine Learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. -Jobs can be created per transaction type, and are based on the service's average response time. +The Machine learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. +With this integration, you can quickly pinpoint anomalous transactions and see the health of +any upstream and downstream services. -After a machine learning job is created, results are shown in two places: +Machine learning jobs are created per environment, and are based on a service's average response time. +Because jobs are created at the environment level, +you can add new services to your existing environments without the need for additional machine learning jobs. -The transaction duration graph will show the expected bounds and add an annotation when the anomaly score is 75 or above. +After a machine learning job is created, results are shown in two places: +* The transaction duration chart will show the expected bounds and add an annotation when the anomaly score is 75 or above. ++ [role="screenshot"] image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in the APM app] -Service maps will display a color-coded anomaly indicator based on the detected anomaly score. - +* Service maps will display a color-coded anomaly indicator based on the detected anomaly score. ++ [role="screenshot"] image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] [float] [[create-ml-integration]] -=== Create a new machine learning job +=== Enable anomaly detection + +To enable machine learning anomaly detection: + +. From the Services overview, Traces overview, or Service Map tab, +select **Anomaly detection**. + +. Click **Create ML Job**. -To enable machine learning anomaly detection, first choose a service to monitor. -Then, select **Integrations** > **Enable ML anomaly detection** and click **Create job**. +. Machine learning jobs are created at the environment level. +Select all of the service environments that you want to enable anomaly detection in. +Anomalies will surface for all services and transaction types within the selected environments. + +. Click **Create Jobs**. That's it! After a few minutes, the job will begin calculating results; -it might take additional time for results to appear on your graph. -Jobs can be managed in *Machine Learning jobs management*. +it might take additional time for results to appear on your service maps. +Existing jobs can be managed in *Machine Learning jobs management*. APM specific anomaly detection wizards are also available for certain Agents. See the machine learning {ml-docs}/ootb-ml-jobs-apm.html[APM anomaly detection configurations] for more information. + +[float] +[[warning-ml-integration]] +=== Anomaly detection warning + +To make machine learning as easy as possible to set up, +the APM app will warn you when filtered to an environment without a machine learning job. + +[role="screenshot"] +image::apm/images/apm-anomaly-alert.png[Example view of anomaly alert in the APM app] \ No newline at end of file diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md new file mode 100644 index 0000000000000..c9f8ab11f6b12 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) > [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) + +## KibanaRequestEvents.completed$ property + +Observable that emits once if and when the request has been completely handled. + +Signature: + +```typescript +completed$: Observable; +``` + +## Remarks + +The request may be considered completed if: - A response has been sent to the client; or - The request was aborted. + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md index 21826c8b29383..dfd7efd27cb5a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md @@ -17,4 +17,5 @@ export interface KibanaRequestEvents | Property | Type | Description | | --- | --- | --- | | [aborted$](./kibana-plugin-core-server.kibanarequestevents.aborted_.md) | Observable<void> | Observable that emits once if and when the request has been aborted. | +| [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) | Observable<void> | Observable that emits once if and when the request has been completely handled. | 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 4d2fac0287035..30e980b5ffc54 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 385d4f6a238d4..3601a00c48cf3 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 85abd9d9dba98..1a94a709cc214 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 6bf481841f334..1bcd575803f88 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 0000000000000..d867509e915b6 --- /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 0000000000000..212133588f62d --- /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 0000000000000..b58f440c7dccf --- /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 0000000000000..ad5c61b5c85a1 --- /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 0097bf8c648f0..0000000000000 --- 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 0000000000000..b7d3d09139a89 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 0000000000000..d63402e8df2fb --- /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 01be8c2e264c5..abbdbeb68d9cb 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 1704a80847652..bc96463f6efba 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 27be7ad28b81a..300f1cba7eb7d 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 884614c8b9551..4008cf852c3a8 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 4fbbc920c4447..e6eb5de31abd8 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 7607e270b5b4f..578108fce51fa 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 aba4451622dcd..0000000000000 --- 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 8fb7559f3e22f..0000000000000 --- 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 89cde2c1cd064..5f17a9b38f9fb 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 29922944e8817..39cf2120baf0a 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 a823f66cf767b..702ad16144e7b 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 9350b9464242a..d2d19dcd87cca 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 f97646e2bbbd3..afc2dc8952c87 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 0e588ab36238b..45598ff8831b0 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 ae5d2b5fb3292..820b13629697d 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 0a9977463aee8..7b43d106417b2 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 d630fec652a37..3f34742e44861 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/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 2d018f7f464b5..3a7335583296e 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -23,6 +23,7 @@ import { HttpService } from '../http_service'; import { contextServiceMock } from '../../context/context_service.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createHttpServer } from '../test_utils'; +import { schema } from '@kbn/config-schema'; let server: HttpService; @@ -195,6 +196,96 @@ describe('KibanaRequest', () => { expect(nextSpy).toHaveBeenCalledTimes(0); expect(completeSpy).toHaveBeenCalledTimes(1); }); + + it('does not complete before response has been sent', async () => { + const { server: innerServer, createRouter, registerOnPreAuth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + registerOnPreAuth((req, res, toolkit) => { + req.events.aborted$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + return toolkit.next(); + }); + + router.post( + { path: '/', validate: { body: schema.any() } }, + async (context, request, res) => { + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + } + ); + + await server.start(); + + await supertest(innerServer.listener).post('/').send({ data: 'test' }).expect(200); + + expect(nextSpy).toHaveBeenCalledTimes(0); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('completed$', () => { + it('emits once and completes when response is sent', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + await supertest(innerServer.listener).get('/').expect(200); + expect(nextSpy).toHaveBeenCalledTimes(1); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + + it('emits once and completes when response is aborted', async (done) => { + expect.assertions(2); + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: () => { + expect(nextSpy).toHaveBeenCalledTimes(1); + done(); + }, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + await delay(30000); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + const incomingRequest = supertest(innerServer.listener) + .get('/') + // end required to send request + .end(); + setTimeout(() => incomingRequest.abort(), 50); + }); }); }); }); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 0e73431fe7c6d..93ffb5aa48259 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -64,6 +64,16 @@ export interface KibanaRequestEvents { * Observable that emits once if and when the request has been aborted. */ aborted$: Observable; + + /** + * Observable that emits once if and when the request has been completely handled. + * + * @remarks + * The request may be considered completed if: + * - A response has been sent to the client; or + * - The request was aborted. + */ + completed$: Observable; } /** @@ -186,11 +196,16 @@ export class KibanaRequest< private getEvents(request: Request): KibanaRequestEvents { const finish$ = merge( - fromEvent(request.raw.req, 'end'), // all data consumed + fromEvent(request.raw.res, 'finish'), // Response has been sent fromEvent(request.raw.req, 'close') // connection was closed ).pipe(shareReplay(1), first()); + + const aborted$ = fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)); + const completed$ = merge(finish$, aborted$).pipe(shareReplay(1), first()); + return { - aborted$: fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)), + aborted$, + completed$, } as const; } diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 95230b52c5c03..eab29731ea524 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -10,37 +10,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -83,37 +64,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -156,37 +118,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -233,37 +176,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -306,37 +230,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -379,37 +284,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -452,37 +338,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -525,37 +392,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -600,37 +448,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -673,37 +502,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index d1c527aca4dba..7caf4af850c10 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -30,17 +30,18 @@ const INJECTED_METADATA = { branch: expect.any(String), buildNumber: expect.any(Number), env: { - binDir: expect.any(String), - configDir: expect.any(String), - homeDir: expect.any(String), - logDir: expect.any(String), + mode: { + name: expect.any(String), + dev: expect.any(Boolean), + prod: expect.any(Boolean), + }, packageInfo: { branch: expect.any(String), buildNum: expect.any(Number), buildSha: expect.any(String), + dist: expect.any(Boolean), version: expect.any(String), }, - pluginSearchPaths: expect.any(Array), }, legacyMetadata: { branch: expect.any(String), diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 8f87d62496891..f49952ec713fb 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -55,7 +55,10 @@ export class RenderingService implements CoreService; + completed$: Observable; } // @public diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index beb5ad40229df..48625078e9bd1 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 c8489673b83af..79279997671e5 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/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index 8facbb709cc5c..449fc4e75fce8 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -22,7 +22,7 @@ pidfile="/var/run/kibana/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name -export KIBANA_PATH_CONF +export KBN_PATH_CONF export NODE_OPTIONS [ -z "$nice" ] && nice=0 diff --git a/src/dev/ci_setup/checkout_sibling_es.sh b/src/dev/ci_setup/checkout_sibling_es.sh index 915759d4214f9..3832ec9b4076a 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 86927b694679a..72ec73ad810e6 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 37bbcce72e497..0bef5bc5f32d4 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 404ad67174681..36d0ff8f51d88 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/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index e1dadb0a24de1..625c2c02510db 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/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 0221c7e0ea085..2cd780d21f681 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -19,6 +19,7 @@ import Joi from 'joi'; import boom from 'boom'; +import { defaultsDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { wrapAuthConfig } from '../../wrap_auth_config'; import { getKibanaInfoForStats } from '../../lib'; @@ -120,10 +121,9 @@ export function registerStatsApi(usageCollection, server, config, kbnServer) { }, }; } else { - accum = { - ...accum, - [usageKey]: usage[usageKey], - }; + // I don't think we need to it this for the above conditions, but do it for most as it will + // match the behavior done in monitoring/bulk_uploader + defaultsDeep(accum, { [usageKey]: usage[usageKey] }); } return accum; diff --git a/src/legacy/core_plugins/kibana/common/utils/no_white_space.js b/src/plugins/dashboard/config.ts similarity index 57% rename from src/legacy/core_plugins/kibana/common/utils/no_white_space.js rename to src/plugins/dashboard/config.ts index 580418eb3423f..ff968a51679e0 100644 --- a/src/legacy/core_plugins/kibana/common/utils/no_white_space.js +++ b/src/plugins/dashboard/config.ts @@ -17,21 +17,10 @@ * under the License. */ -const TAGS_WITH_WS = />\s+<'); -} +export const configSchema = schema.object({ + allowByValueEmbeddables: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; 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 e4a98ffac7a55..0000f63c48c2d 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/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index f0b57fec169fd..f1319665d258b 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -94,6 +94,10 @@ declare module '../../share/public' { export type DashboardUrlGenerator = UrlGeneratorContract; +interface DashboardFeatureFlagConfig { + allowByValueEmbeddables: boolean; +} + interface SetupDependencies { data: DataPublicPluginSetup; embeddable: EmbeddableSetup; @@ -125,6 +129,7 @@ export interface DashboardStart { embeddableType: string; }) => void | undefined; dashboardUrlGenerator?: DashboardUrlGenerator; + dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; } @@ -411,6 +416,7 @@ export class DashboardPlugin getSavedDashboardLoader: () => savedDashboardLoader, addEmbeddableToDashboard: this.addEmbeddableToDashboard.bind(this, core), dashboardUrlGenerator: this.dashboardUrlGenerator, + dashboardFeatureFlagConfig: this.initializerContext.config.get(), DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts index 9719586001c59..3ef7abba5776b 100644 --- a/src/plugins/dashboard/server/index.ts +++ b/src/plugins/dashboard/server/index.ts @@ -17,8 +17,16 @@ * under the License. */ -import { PluginInitializerContext } from '../../../core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../core/server'; import { DashboardPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + allowByValueEmbeddables: true, + }, + schema: configSchema, +}; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d6812a4aa4524..2cfdab80123ed 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 307d1fe1b2b0b..2053e0b94b21a 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 df2fbc8e5a8f3..35b46de5f21b2 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 a9ca9efb8b7e1..aaaac5ae6ff7c 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 cb1b2b65c17c8..7adb0c3caa675 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 bb85532fd3ab5..3e98f901eb0c3 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 84e24114a9e6c..e6eca16c5ca4b 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 5f2d4c00cd6b6..879ff6708068e 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 86bf30ba0e374..05249d46a1c50 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 461b21e1cc980..1f3d7fbcb9f0f 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 ca5ba1b90fe95..417dc1c2012d3 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 38fb517e3c3f6..0000000000000 --- 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 c43c572c2edbb..e1be92aa13c37 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 b8010f735c327..78ead6df1a44e 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 67789fcbf56b4..cea2714671f0b 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 bf1982a1f7fb2..32d8f8c1b09e0 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 bbd0671754749..9dc47369567af 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 25dc890e0257d..76afd7e8c951c 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 d35a6a5bbb9a9..013034c79d3f3 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 e825ef7f6c945..763a086d7688d 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/public/application/angular/context/api/utils/date_conversion.ts b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts index 64544a335c911..4369234a3ce9a 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts @@ -31,15 +31,6 @@ export function extractNanos(timeFieldValue: string = ''): string { return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds; } -/** - * extract the nanoseconds as string of a given ISO formatted timestamp - */ -export function convertIsoToNanosAsStr(isoValue: string): string { - const nanos = extractNanos(isoValue); - const millis = convertIsoToMillis(isoValue); - return `${millis}${nanos.substr(3, 6)}`; -} - /** * convert an iso formatted string to number of milliseconds since * 1970-01-01T00:00:00.000Z diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index d4ee9e0e0f287..24ac19a7e3bc3 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { convertIsoToNanosAsStr } from './date_conversion'; import { SurrDocType, EsHitRecordList, EsHitRecord } from '../context'; export type EsQuerySearchAfter = [string | number, string | number]; @@ -38,15 +37,10 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - const afterTimeValue = nanoSeconds - ? convertIsoToNanosAsStr(afterTimeDoc.fields[timeFieldName][0]) - : afterTimeDoc.sort[0]; + const afterTimeValue = nanoSeconds ? afterTimeDoc._source[timeFieldName] : afterTimeDoc.sort[0]; return [afterTimeValue, afterTimeDoc.sort[1]]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string - return [ - nanoSeconds ? convertIsoToNanosAsStr(anchor.fields[timeFieldName][0]) : anchor.sort[0], - anchor.sort[1], - ]; + return [nanoSeconds ? anchor._source[timeFieldName] : anchor.sort[0], anchor.sort[1]]; } 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 7b862ec518a04..e7fafde2e68d0 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/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 3163c4bde4704..c9694ad7b9423 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -4,7 +4,6 @@ "server": false, "ui": true, "requiredPlugins": [ - "data", "inspector", "uiActions" ], diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 48e5483124704..6b451e71522c5 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -102,8 +102,6 @@ const createStartContract = (): Start => { getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), - filtersAndTimeRangeFromContext: jest.fn(), - filtersFromContext: jest.fn(), }; return startContract; }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 508c82c4247ed..319cbf8ec44b4 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -17,13 +17,7 @@ * under the License. */ import React from 'react'; -import { - DataPublicPluginSetup, - DataPublicPluginStart, - Filter, - TimeRange, - esFilters, -} from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -44,9 +38,6 @@ import { IEmbeddable, EmbeddablePanel, SavedObjectEmbeddableInput, - ChartActionContext, - isRangeSelectTriggerContext, - isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { AttributeService } from './lib/embeddables/attribute_service'; @@ -92,18 +83,6 @@ export interface EmbeddableStart { type: string ) => AttributeService; - /** - * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. - */ - filtersFromContext: (context: ChartActionContext) => Promise; - - /** - * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object. - */ - filtersAndTimeRangeFromContext: ( - context: ChartActionContext - ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>; - EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -155,41 +134,6 @@ export class EmbeddablePublicPlugin implements Plugin { - try { - if (isRangeSelectTriggerContext(context)) - return await data.actions.createFiltersFromRangeSelectAction(context.data); - if (isValueClickTriggerContext(context)) - return await data.actions.createFiltersFromValueClickAction(context.data); - // eslint-disable-next-line no-console - console.warn("Can't extract filters from action.", context); - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Error extracting filters from action. Returning empty filter list.', error); - } - return []; - }; - - const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async ( - context - ) => { - const filters = await filtersFromContext(context); - - if (!context.data.timeFieldName) return { filters }; - - const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.data.timeFieldName, - filters - ); - - return { - filters: restOfFilters, - timeRange: timeRangeFilter - ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter) - : undefined, - }; - }; - const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({ embeddable, hideHeader, @@ -216,8 +160,6 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), - filtersFromContext, - filtersAndTimeRangeFromContext, getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index ae40b2c92d40e..833304378402a 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 6e416f7fd5c84..b924c3bb53056 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 0a2a18c7cef4f..648193e8e2490 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 8bffc5d012a74..ad19def160200 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 cf906bc5c86cf..079f510bb256a 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 435e52103d156..e754c84291116 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 9ff25ce674d3d..f2830e58e0eea 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 3381553fe9364..4bf8c98c99d2c 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/ui_actions/public/triggers/apply_filter_trigger.ts b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts index 7a95709ac28ba..fa9ace1a36c69 100644 --- a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts @@ -22,6 +22,6 @@ import { Trigger } from '.'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { id: APPLY_FILTER_TRIGGER, - title: 'Filter click', + title: 'Apply filter', description: 'Triggered when user applies filter to an embeddable.', }; diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index a9b542af68c9d..dd748ea2d3815 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -42,5 +42,7 @@ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { migrations: { '7.7.0': flow(resetCount), '7.8.0': flow(resetCount), + '7.9.0': flow(resetCount), + '7.10.0': flow(resetCount), }, }; diff --git a/tasks/test_jest.js b/tasks/test_jest.js index d8f51806e8ddc..810ed42324840 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/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index 89769caaea253..cdf2d6c04be83 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -30,8 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // FLAKY/FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos', () => { + describe('context view for date_nanos', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 6329f6c431e6a..8fe08d13af0aa 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -30,10 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // skipped due to a recent change in ES that caused search_after queries with data containing - // custom timestamp formats like in the testdata to fail - // https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos with custom timestamp', () => { + describe('context view for date_nanos with custom timestamp', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 7e1f88650cbba..f1082bf618b90 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 0000000000000..503d12b2f6d73 --- /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 0000000000000..513664263791b --- /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 0000000000000..7a6fd46c46c76 --- /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 0000000000000..a08d7d07a24a1 --- /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 0000000000000..1d43d32c9feb8 --- /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 0000000000000..c74ec295b385c --- /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 0000000000000..9184758577654 --- /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 0000000000000..5f9aafe80e10e --- /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 0000000000000..d667c753baec2 --- /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 0000000000000..07c49638134be --- /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 0000000000000..b73a71e7ff7fd --- /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 0000000000000..9f8343e540861 --- /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 c122d71b58edb..fa7cbd41d7078 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 2310a35f94f33..f449986713f97 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 0000000000000..0c3ee4e3f261f --- /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 60d7f0406f4c9..2542d7032e83b 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 2bba6e06d76d7..247ab360b7912 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 0000000000000..1d691d98982de --- /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 204911a3eedaa..a5a1a2103801f --- 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 0000000000000..5274d05572e71 --- /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 49ee8a6b526ca..05b88aa2dd0a2 --- 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 7bbb867526384..b7eac33f35176 --- 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 a72e9749ebbd5..74a3de77e3a76 --- 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 a3c03dd780886..3afd4bfb76396 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 c962b962b1e5e..2452e2f5b8c58 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 0000000000000..3fd3d02de1304 --- /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 d3ca8839a7dab..e3b0fe778bdfb 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 7fb7d7b71b2e4..55d4a524820c5 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 0000000000000..c3211300b96c5 --- /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 0000000000000..b9c683bcb049e --- /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 0000000000000..152c97a3ca7df --- /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 0000000000000..73dbbddfb38f6 --- /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 0000000000000..e25452698cebc --- /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 0000000000000..e9985300ba19d --- /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 0000000000000..43c00f0a09dcf --- /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 0000000000000..4d7f9c28210d1 --- /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 0000000000000..93d70ec355391 --- /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 0000000000000..9078f01f1b870 --- /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 0000000000000..493fe9f58d322 --- /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 0000000000000..b21301f25ad08 --- /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 460a90b8ec0c0..2a1b55d832606 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/githubPr.groovy b/vars/githubPr.groovy index da5348749f668..ec3dbd919fed6 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -15,7 +15,7 @@ */ def withDefaultPrComments(closure) { catchErrors { - // sendCommentOnError() needs to know if comments are enabled, so lets track it with a global + // kibanaPipeline.notifyOnError() needs to know if comments are enabled, so lets track it with a global // isPr() just ensures this functionality is skipped for non-PR builds buildState.set('PR_COMMENTS_ENABLED', isPr()) catchErrors { @@ -59,19 +59,6 @@ def sendComment(isFinal = false) { } } -def sendCommentOnError(Closure closure) { - try { - closure() - } catch (ex) { - // If this is the first failed step, it's likely that the error hasn't propagated up far enough to mark the build as a failure - currentBuild.result = 'FAILURE' - catchErrors { - sendComment(false) - } - throw ex - } -} - // Checks whether or not this currently executing build was triggered via a PR in the elastic/kibana repo def isPr() { return !!(env.ghprbPullId && env.ghprbPullLink && env.ghprbPullLink =~ /\/elastic\/kibana\//) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 410578886a01d..173c5b7e11764 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -2,50 +2,119 @@ 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 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 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('/') + } + } + } - 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", - ]) { - githubPr.sendCommentOnError { - closure() + return workspaces +} + +def notifyOnError(Closure closure) { + try { + closure() + } catch (ex) { + // If this is the first failed step, it's likely that the error hasn't propagated up far enough to mark the build as a failure + currentBuild.result = 'FAILURE' + catchErrors { + githubPr.sendComment(false) + } + catchErrors { + // an empty map is a valid config, but is falsey, so let's use .has() + if (buildState.has('SLACK_NOTIFICATION_CONFIG')) { + slackNotifications.sendFailedBuild(buildState.get('SLACK_NOTIFICATION_CONFIG')) } } + throw ex + } +} + +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 + + 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}") + } } } } @@ -101,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', ] @@ -121,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}") + } + } } } }) @@ -133,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() { @@ -182,7 +268,7 @@ def bash(script, label) { } def doSetup() { - githubPr.sendCommentOnError { + notifyOnError { retryWithDelay(2, 15) { try { runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") @@ -198,26 +284,36 @@ def doSetup() { } } -def buildOss() { - githubPr.sendCommentOnError { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") +def buildOss(maxWorkers = '') { + notifyOnError { + withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + } } } -def buildXpack() { - githubPr.sendCommentOnError { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") +def buildXpack(maxWorkers = '') { + notifyOnError { + 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" ) @@ -256,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) { @@ -266,5 +458,4 @@ def pipelineLibraryTests() { } } - return this diff --git a/vars/slackNotifications.groovy b/vars/slackNotifications.groovy index 30f86e6d6f0ad..02aad14d8ba3f 100644 --- a/vars/slackNotifications.groovy +++ b/vars/slackNotifications.groovy @@ -105,16 +105,26 @@ def getDefaultDisplayName() { return "${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}" } -def getDefaultContext() { - def duration = currentBuild.durationString.replace(' and counting', '') +def getDefaultContext(config = [:]) { + def progressMessage = "" + if (config && !config.isFinal) { + progressMessage = "In-progress" + } else { + def duration = currentBuild.durationString.replace(' and counting', '') + progressMessage = "${buildUtils.getBuildStatus().toLowerCase().capitalize()} after ${duration}" + } return contextBlock([ - "${buildUtils.getBuildStatus().toLowerCase().capitalize()} after ${duration}", + progressMessage, "", ].join(' · ')) } -def getStatusIcon() { +def getStatusIcon(config = [:]) { + if (config && !config.isFinal) { + return ':hourglass_flowing_sand:' + } + def status = buildUtils.getBuildStatus() if (status == 'UNSTABLE') { return ':yellow_heart:' @@ -124,7 +134,7 @@ def getStatusIcon() { } def getBackupMessage(config) { - return "${getStatusIcon()} ${config.title}\n\nFirst attempt at sending this notification failed. Please check the build." + return "${getStatusIcon(config)} ${config.title}\n\nFirst attempt at sending this notification failed. Please check the build." } def sendFailedBuild(Map params = [:]) { @@ -135,19 +145,32 @@ def sendFailedBuild(Map params = [:]) { color: 'danger', icon: ':jenkins:', username: 'Kibana Operations', - context: getDefaultContext(), + isFinal: false, ] + params - def title = "${getStatusIcon()} ${config.title}" - def message = "${getStatusIcon()} ${config.message}" + config.context = config.context ?: getDefaultContext(config) + + def title = "${getStatusIcon(config)} ${config.title}" + def message = "${getStatusIcon(config)} ${config.message}" def blocks = [markdownBlock(title)] getFailedBuildBlocks().each { blocks << it } blocks << dividerBlock() blocks << config.context + def channel = config.channel + def timestamp = null + + def previousResp = buildState.get('SLACK_NOTIFICATION_RESPONSE') + if (previousResp) { + // When using `timestamp` to update a previous message, you have to use the channel ID from the previous response + channel = previousResp.channelId + timestamp = previousResp.ts + } + def resp = slackSend( - channel: config.channel, + channel: channel, + timestamp: timestamp, username: config.username, iconEmoji: config.icon, color: config.color, @@ -156,7 +179,7 @@ def sendFailedBuild(Map params = [:]) { ) if (!resp) { - slackSend( + resp = slackSend( channel: config.channel, username: config.username, iconEmoji: config.icon, @@ -165,6 +188,10 @@ def sendFailedBuild(Map params = [:]) { blocks: [markdownBlock(getBackupMessage(config))] ) } + + if (resp) { + buildState.set('SLACK_NOTIFICATION_RESPONSE', resp) + } } def onFailure(Map options = [:]) { @@ -172,6 +199,7 @@ def onFailure(Map options = [:]) { def status = buildUtils.getBuildStatus() if (status != "SUCCESS") { catchErrors { + options.isFinal = true sendFailedBuild(options) } } @@ -179,6 +207,16 @@ def onFailure(Map options = [:]) { } def onFailure(Map options = [:], Closure closure) { + if (options.disabled) { + catchError { + closure() + } + + return + } + + buildState.set('SLACK_NOTIFICATION_CONFIG', options) + // try/finally will NOT work here, because the build status will not have been changed to ERROR when the finally{} block executes catchError { closure() diff --git a/vars/task.groovy b/vars/task.groovy new file mode 100644 index 0000000000000..0c07b519b6fef --- /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 0000000000000..52641ce31f0be --- /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 0000000000000..8132d6264744f --- /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 74ce86516e863..e582e996a78b5 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'), @@ -126,7 +134,7 @@ def intake(jobName, String script) { return { ci(name: jobName, size: 's-highmem', ramDisk: true) { withEnv(["JOB=${jobName}"]) { - githubPr.sendCommentOnError { + kibanaPipeline.notifyOnError { runbld(script, "Execute ${jobName}") } } @@ -169,7 +177,9 @@ def parallelProcesses(Map params) { sleep(delay) } - processClosure(processNumber) + withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) { + processClosure() + } } } diff --git a/x-pack/package.json b/x-pack/package.json index e3104aabbb02b..dcba01a771fd5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -75,6 +75,7 @@ "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", "@types/jest": "^25.2.3", + "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", "@types/js-search": "^1.4.0", "@types/js-yaml": "^3.11.1", @@ -119,6 +120,7 @@ "@types/xml2js": "^0.4.5", "@types/stats-lite": "^2.2.0", "@types/pretty-ms": "^5.0.0", + "@types/webpack-env": "^1.15.2", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "autoprefixer": "^9.7.4", @@ -158,6 +160,7 @@ "jest-cli": "^25.5.4", "jest-styled-components": "^7.0.2", "jsdom": "13.1.0", + "jsondiffpatch": "0.4.1", "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", @@ -394,4 +397,4 @@ "cypress-multi-reporters" ] } -} +} \ No newline at end of file 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 ce5c1fe8500fb..b25e33400df5d 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 1f7409fedd2c2..4015381ff9502 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 90b989ac3b52e..16a5a59882dd6 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 1bfe881cdfdee..26a13ed83cc28 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -307,7 +307,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 676a4776d0055..82dedb09c429e 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 1a24622e1cabb..195f6db538ae5 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 7ddb123a4d780..a51a0432a01e0 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 be60f4c2f28af..7a0e24521a1c6 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 899684367d52d..53bf75651b1e5 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 b1ed3728edfae..c379c05ee88e3 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 0c8802060164d..b76e57419bc56 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 d5a9c0cc1ccd2..e4828f33765ce 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 bf8a3d8032cc5..490764fb16bfd 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 e62ca465f30f8..109008b8fc9fb 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 d1a739c2304f2..6d4176067c3ba 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 55c373f14cd69..209582585256b 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 53b17f58d6e18..26dd8a1a1402a 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 0b8b27b278928..be75742fa882e 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 0e63cc8f5956e..bce06c829b1bc 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 0a474ec3ae3ea..32c3c54faf007 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 9204c41b9288c..10a8501e856d2 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 03ae7a9b35a81..10c688c075eab 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 021c460f4c815..50231f1c9a3a1 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 ac4b332e7fd7a..ca93e88d01203 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 5b8b25d02658b..54d137cc0f617 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 38fca656bef5a..b668e3460828a 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 0d49d9a3a256e..f15a117106210 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 a8e19e3ff2e79..ecec45ade0460 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/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 3e8b0ba0e8b5e..cd8bcaa1de237 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -8,4 +8,18 @@ export enum ProcessorEvent { transaction = 'transaction', error = 'error', metric = 'metric', + span = 'span', + onboarding = 'onboarding', + sourcemap = 'sourcemap', } +/** + * Processor events that are searchable in the UI via the query bar. + * + * Some client-sideroutes will define 1 or more processor events that + * will be used to fetch the dynamic index pattern for the query bar. + */ + +export type UIProcessorEvent = + | ProcessorEvent.transaction + | ProcessorEvent.error + | ProcessorEvent.metric; diff --git a/x-pack/plugins/apm/common/projections.ts b/x-pack/plugins/apm/common/projections.ts new file mode 100644 index 0000000000000..a5fd9d3951cc9 --- /dev/null +++ b/x-pack/plugins/apm/common/projections.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum Projection { + services = 'services', + transactionGroups = 'transactionGroups', + traces = 'traces', + transactions = 'transactions', + metrics = 'metrics', + errorGroups = 'errorGroups', + serviceNodes = 'serviceNodes', + rumOverview = 'rumOverview', +} diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts deleted file mode 100644 index 809caeeaf6088..0000000000000 --- a/x-pack/plugins/apm/common/projections/services.ts +++ /dev/null @@ -1,64 +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 { - Setup, - SetupUIFilters, - SetupTimeRange, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../server/lib/helpers/setup_request'; -import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; - -export function getServicesProjection({ - setup, - noEvents, -}: { - setup: Setup & SetupTimeRange & SetupUIFilters; - noEvents?: boolean; -}) { - const { start, end, uiFiltersES, indices } = setup; - - return { - ...(noEvents - ? {} - : { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], - }), - body: { - size: 0, - query: { - bool: { - filter: [ - ...(noEvents - ? [] - : [ - { - terms: { - [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'], - }, - }, - ]), - { range: rangeFilter(start, end) }, - ...uiFiltersES, - ], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index fe2303d645ec9..92ea044720531 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -14,7 +14,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useFetcher } from '../../../hooks/useFetcher'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -79,7 +79,7 @@ function ErrorGroupOverview() { params: { serviceName, }, - projection: PROJECTION.ERROR_GROUPS, + projection: Projection.errorGroups, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index c6c0861c26a34..b2f15dbb11341 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -84,7 +84,7 @@ interface Props { export function Home({ tab }: Props) { const { config, core } = useApmPluginContext(); - const isMLEnabled = !!core.application.capabilities.ml; + const canAccessML = !!core.application.capabilities.ml?.canAccessML; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -106,7 +106,7 @@ export function Home({ tab }: Props) { - {isMLEnabled && ( + {canAccessML && ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 9b88202b2e5ef..8d1959ec14d15 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -28,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], - projection: PROJECTION.RUM_OVERVIEW, + projection: Projection.rumOverview, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 9af6a8d988c11..9b01f9ebb7e99 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -16,7 +16,7 @@ import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from '../../shared/charts/MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { @@ -36,7 +36,7 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) { serviceName, serviceNodeName, }, - projection: PROJECTION.METRICS, + projection: Projection.metrics, showCount: false, }), [serviceName, serviceNodeName] diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 5537a73d228e8..3cde48aa483cb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; @@ -46,7 +46,7 @@ function ServiceNodeOverview() { params: { serviceName, }, - projection: PROJECTION.SERVICE_NODES, + projection: Projection.serviceNodes, }), [serviceName] ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7d05ae90afb87..7146e471a7f82 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -15,7 +15,7 @@ import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; @@ -88,7 +88,7 @@ export function ServiceOverview() { const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'agentName'], - projection: PROJECTION.SERVICES, + projection: Projection.services, }), [] ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 1471bc345d850..cb4726244e50c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -20,7 +20,7 @@ import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { const plugin = useApmPluginContext(); - const isMLEnabled = !!plugin.core.application.capabilities.ml; + const canAccessML = !!plugin.core.application.capabilities.ml?.canAccessML; const { search, pathname } = useLocation(); return ( <> @@ -51,7 +51,7 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - ...(isMLEnabled + ...(canAccessML ? [ { name: i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cdebb3aac129b..06b4459fb56eb 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -11,7 +11,7 @@ import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; @@ -48,7 +48,7 @@ export function TraceOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'host', 'containerId', 'podName'], - projection: PROJECTION.TRACES, + projection: Projection.traces, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index c4d5be5874215..0dc2f607b1ef2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -27,7 +27,7 @@ import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; @@ -52,7 +52,7 @@ export function TransactionDetails() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'serviceVersion'], - projection: PROJECTION.TRANSACTIONS, + projection: Projection.transactions, params: { transactionName, transactionType, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 98702fe3686ff..d9bd3e59d281f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -35,7 +35,7 @@ import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; @@ -103,7 +103,7 @@ export function TransactionOverview() { serviceName, transactionType, }, - projection: PROJECTION.TRANSACTION_GROUPS, + projection: Projection.transactionGroups, }), [serviceName, transactionType] ); 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 36e33fba89fbb..2434d898389d8 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 5201d80de5a12..403a8cad854cd 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/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 502f5f0034b5f..6c605886e6e00 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; -// @ts-ignore +// @ts-expect-error import { Typeahead } from './Typeahead'; import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; 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 a80dcca4a107d..0c209b0aca917 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/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index fedf96b4cc4ea..ba700e68b59bc 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -17,10 +17,10 @@ import styled from 'styled-components'; import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; interface Props { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; showCount?: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 61632700b81d8..5b167e8160ffa 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -7,7 +7,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; -// @ts-ignore +// @ts-expect-error import CustomPlot from '../CustomPlot'; import { asDecimal, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts index d9781400f2272..65514ff71d02b 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -7,10 +7,13 @@ import { compact, pickBy } from 'lodash'; import datemath from '@elastic/datemath'; import { IUrlParams } from './types'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; interface PathParams { - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; serviceName?: string; errorGroupId?: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts index 78fe662b88d75..7b50a705afa33 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -6,7 +6,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { UIProcessorEvent } from '../../../common/processor_event'; export type IUrlParams = { detailTab?: string; @@ -32,6 +32,6 @@ export type IUrlParams = { pageSize?: number; serviceNodeName?: string; searchTerm?: string; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; traceIdLink?: string; } & Partial>; diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index 64f333d72f0f5..0b4978acdfcb1 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -5,10 +5,10 @@ */ import { useFetcher } from './useFetcher'; -import { ProcessorEvent } from '../../common/processor_event'; +import { UIProcessorEvent } from '../../common/processor_event'; export function useDynamicIndexPattern( - processorEvent: ProcessorEvent | undefined + processorEvent: UIProcessorEvent | undefined ) { const { data, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 3354e676cf323..45ede7e7f2607 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -17,7 +17,7 @@ import { import { history } from '../utils/history'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { PROJECTION } from '../../common/projections/typings'; +import { Projection } from '../../common/projections'; import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; @@ -35,7 +35,7 @@ export function useLocalUIFilters({ filterNames, params, }: { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; }) { diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 418312743c324..e750102de2baa 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -99,10 +99,9 @@ export function expectTextsInDocument(output: any, texts: string[]) { } interface MockSetup { - dynamicIndexPattern: any; start: number; end: number; - client: any; + apmEventClient: any; internalClient: any; config: APMConfig; uiFiltersES: ESFilter[]; @@ -148,7 +147,7 @@ export async function inspectSearchParams( const mockSetup = { start: 1528113600000, end: 1528977600000, - client: { search: spy } as any, + apmEventClient: { search: spy } as any, internalClient: { search: spy } as any, config: new Proxy({}, { get: () => 'myIndex' }) as APMConfig, uiFiltersES: [{ term: { 'my.custom.ui.filter': 'foo-bar' } }], diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap index b943102b39de8..da2309afa07cf 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -2,6 +2,13 @@ exports[`getAllEnvironments fetches all environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -15,15 +22,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -34,16 +32,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`getAllEnvironments fetches all environments with includeMissing 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -57,15 +57,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -76,10 +67,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 9b17033a1f2a5..423b87cb78c3c 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; @@ -21,7 +21,7 @@ export async function getAllEnvironments({ setup: Setup; includeMissing?: boolean; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; // omit filter for service.name if "All" option is selected const serviceNameFilter = serviceName @@ -29,21 +29,18 @@ export async function getAllEnvironments({ : []; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ...serviceNameFilter, - ], + filter: [...serviceNameFilter], }, }, aggs: { @@ -58,7 +55,7 @@ export async function getAllEnvironments({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const environments = resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string diff --git a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index 982ad558dc91d..63b6c9cde4d0d 100644 --- a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error queries fetches a single error group 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "query": Object { "bool": Object { @@ -11,11 +16,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "error.grouping_key": "groupId", @@ -57,12 +57,16 @@ Object { }, ], }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -104,11 +108,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -180,11 +183,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -204,6 +202,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap index b71b2d697126a..ea142ca2acc00 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error distribution queries fetches an error distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -19,11 +24,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -48,12 +48,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error distribution queries fetches an error distribution with a group id 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -71,11 +75,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -105,6 +104,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap index d336d71424750..085bedf774c46 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should make the correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -21,11 +26,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -50,7 +50,6 @@ Array [ }, "size": 0, }, - "index": "apm-*", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 5f23a9329a583..e0df4d7744610 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { getBuckets } from '../get_buckets'; import { APMConfig } from '../../../..'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let clientSpy: jest.Mock; @@ -29,7 +29,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { + apmEventClient: { search: clientSpy, } as any, internalClient: { @@ -66,8 +66,6 @@ describe('timeseriesFetcher', () => { it('should limit query results to error documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([{ term: { [PROCESSOR_EVENT]: 'error' } }]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.error]); }); }); diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index db36ad1ede91c..de6df15354e79 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../../common/utils/range_filter'; @@ -28,9 +28,8 @@ export async function getBuckets({ bucketSize: number; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -41,7 +40,9 @@ export async function getBuckets({ } const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, query: { @@ -65,7 +66,7 @@ export async function getBuckets({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const buckets = (resp.aggregations?.distribution.buckets || []).map( (bucket) => ({ diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 3d20f84ccfbc2..b23c955b57183 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, @@ -32,17 +31,18 @@ export async function getErrorGroup({ groupId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { size: 1, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [ERROR_GROUP_ID]: groupId } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -57,7 +57,7 @@ export async function getErrorGroup({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const error = resp.hits.hits[0]?._source; const transactionId = error?.transaction?.id; const traceId = error?.trace?.id; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index ad216de271f37..ab1c2149be343 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -13,14 +13,13 @@ import { ERROR_LOG_MESSAGE, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getErrorGroupsProjection } from '../../../common/projections/errors'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getErrorGroupsProjection } from '../../projections/errors'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SortOptions } from '../../../typings/elasticsearch/aggregations'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -38,7 +37,7 @@ export async function getErrorGroups({ sortDirection?: 'asc' | 'desc'; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; @@ -92,23 +91,7 @@ export async function getErrorGroups({ }, }); - interface SampleError { - '@timestamp': APMError['@timestamp']; - error: { - log?: { - message: string; - }; - exception?: Array<{ - handled?: boolean; - message?: string; - type?: string; - }>; - culprit: APMError['error']['culprit']; - grouping_key: APMError['error']['grouping_key']; - }; - } - - const resp = await client.search(params); + const resp = await apmEventClient.search(params); // aggregations can be undefined when no matching indices are found. // this is an exception rather than the rule so the ES type does not account for this. diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts new file mode 100644 index 0000000000000..c475640595227 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-console */ + +import chalk from 'chalk'; +import { + LegacyAPICaller, + KibanaRequest, +} from '../../../../../../../src/core/server'; + +function formatObj(obj: Record) { + return JSON.stringify(obj, null, 2); +} + +export async function callClientWithDebug({ + apiCaller, + operationName, + params, + debug, + request, +}: { + apiCaller: LegacyAPICaller; + operationName: string; + params: Record; + debug: boolean; + request: KibanaRequest; +}) { + const startTime = process.hrtime(); + + let res: any; + let esError = null; + try { + res = apiCaller(operationName, params); + } catch (e) { + // catch error and throw after outputting debug info + esError = e; + } + + if (debug) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${ + request.route.path + }`; + + console.log( + chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + ); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); + + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); + } + + if (esError) { + throw esError; + } + + return res; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts new file mode 100644 index 0000000000000..494cd6cbf0eec --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep } from 'lodash'; +import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fieldnames'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; + +/* + Adds a range query to the ES request to exclude legacy data +*/ + +export function addFilterToExcludeLegacyData( + params: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } +) { + const nextParams = cloneDeep(params); + + // add filter for omitting pre-7.x data + nextParams.body.query.bool.filter.push({ + range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, + }); + + return nextParams; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts new file mode 100644 index 0000000000000..2bfd3c94ed34c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ValuesType } from 'utility-types'; +import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESSearchResponse, +} from '../../../../../typings/elasticsearch'; +import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; +import { callClientWithDebug } from '../call_client_with_debug'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { unpackProcessorEvents } from './unpack_processor_events'; + +export type APMEventESSearchRequest = Omit & { + apm: { + events: ProcessorEvent[]; + }; +}; + +type TypeOfProcessorEvent = { + [ProcessorEvent.error]: APMError; + [ProcessorEvent.transaction]: Transaction; + [ProcessorEvent.span]: Span; + [ProcessorEvent.metric]: APMBaseDoc; + [ProcessorEvent.onboarding]: unknown; + [ProcessorEvent.sourcemap]: unknown; +}[T]; + +type ESSearchRequestOf = Omit< + TParams, + 'apm' +> & { index: string[] | string }; + +type TypedSearchResponse< + TParams extends APMEventESSearchRequest +> = ESSearchResponse< + TypeOfProcessorEvent>, + ESSearchRequestOf +>; + +export type APMEventClient = ReturnType; + +export function createApmEventClient({ + context, + request, + indices, + options: { includeFrozen } = { includeFrozen: false }, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; + indices: ApmIndicesConfig; + options: { + includeFrozen: boolean; + }; +}) { + const client = context.core.elasticsearch.legacy.client; + + return { + search( + params: TParams, + { includeLegacyData } = { includeLegacyData: false } + ): Promise> { + const withProcessorEventFilter = unpackProcessorEvents(params, indices); + + const withPossibleLegacyDataFilter = !includeLegacyData + ? addFilterToExcludeLegacyData(withProcessorEventFilter) + : withProcessorEventFilter; + + return callClientWithDebug({ + apiCaller: client.callAsCurrentUser, + operationName: 'search', + params: { + ...withPossibleLegacyDataFilter, + ignore_throttled: !includeFrozen, + }, + request, + debug: context.params.query._debug, + }); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts new file mode 100644 index 0000000000000..d35403ad35d94 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uniq, defaultsDeep, cloneDeep } from 'lodash'; +import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; +import { APMEventESSearchRequest } from '.'; +import { + ApmIndicesConfig, + ApmIndicesName, +} from '../../../settings/apm_indices/get_apm_indices'; + +export const processorEventIndexMap: Record = { + [ProcessorEvent.transaction]: 'apm_oss.transactionIndices', + [ProcessorEvent.span]: 'apm_oss.spanIndices', + [ProcessorEvent.metric]: 'apm_oss.metricsIndices', + [ProcessorEvent.error]: 'apm_oss.errorIndices', + [ProcessorEvent.sourcemap]: 'apm_oss.sourcemapIndices', + [ProcessorEvent.onboarding]: 'apm_oss.onboardingIndices', +}; + +export function unpackProcessorEvents( + request: APMEventESSearchRequest, + indices: ApmIndicesConfig +) { + const { apm, ...params } = request; + + const index = uniq( + apm.events.map((event) => indices[processorEventIndexMap[event]]) + ); + + const withFilterForProcessorEvent: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } = defaultsDeep(cloneDeep(params), { + body: { + query: { + bool: { + filter: [], + }, + }, + }, + }); + + withFilterForProcessorEvent.body.query.bool.filter.push({ + terms: { + [PROCESSOR_EVENT]: apm.events, + }, + }); + + return { + index, + ...withFilterForProcessorEvent, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts new file mode 100644 index 0000000000000..072391606d574 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IndexDocumentParams, + IndicesCreateParams, + DeleteDocumentResponse, + DeleteDocumentParams, +} from 'elasticsearch'; +import { KibanaRequest } from 'src/core/server'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../../typings/elasticsearch'; +import { callClientWithDebug } from '../call_client_with_debug'; + +// `type` was deprecated in 7.0 +export type APMIndexDocumentParams = Omit, 'type'>; + +export type APMInternalClient = ReturnType; + +export function createInternalESClient({ + context, + request, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; +}) { + const { callAsInternalUser } = context.core.elasticsearch.legacy.client; + + const callEs = (operationName: string, params: Record) => { + return callClientWithDebug({ + apiCaller: callAsInternalUser, + operationName, + params, + request, + debug: context.params.query._debug, + }); + }; + + return { + search: async < + TDocument = unknown, + TSearchRequest extends ESSearchRequest = ESSearchRequest + >( + params: TSearchRequest + ): Promise> => { + return callEs('search', params); + }, + index: (params: APMIndexDocumentParams) => { + return callEs('index', params); + }, + delete: ( + params: Omit + ): Promise => { + return callEs('delete', params); + }, + indicesCreate: (params: IndicesCreateParams) => { + return callEs('indices.create', params); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts deleted file mode 100644 index 61c9d751bf533..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isApmIndex } from './es_client'; - -describe('isApmIndex', () => { - const apmIndices = [ - 'apm-*-metric-*', - 'apm-*-onboarding-*', - 'apm-*-span-*', - 'apm-*-transaction-*', - 'apm-*-error-*', - ]; - describe('when indexParam is a string', () => { - it('should return true if it matches any of the items in apmIndices', () => { - const indexParam = 'apm-*-transaction-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it('should return false if it does not match any of the items in `apmIndices`', () => { - const indexParam = '.ml-anomalies-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is an array', () => { - it('should return true if all values in `indexParam` matches values in `apmIndices`', () => { - const indexParam = ['apm-*-transaction-*', 'apm-*-span-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it("should return false if some of the values don't match with `apmIndices`", () => { - const indexParam = ['apm-*-transaction-*', '.ml-anomalies-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is neither a string or an array', () => { - it('should return false', () => { - [true, false, undefined].forEach((indexParam) => { - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts deleted file mode 100644 index 2d730933e2473..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ /dev/null @@ -1,228 +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. - */ - -/* eslint-disable no-console */ -import { - IndexDocumentParams, - SearchParams, - IndicesCreateParams, - DeleteDocumentResponse, - DeleteDocumentParams, -} from 'elasticsearch'; -import { cloneDeep, isString, merge } from 'lodash'; -import { KibanaRequest } from 'src/core/server'; -import chalk from 'chalk'; -import { - ESSearchRequest, - ESSearchResponse, -} from '../../../typings/elasticsearch'; -import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { pickKeys } from '../../../common/utils/pick_keys'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -// `type` was deprecated in 7.0 -export type APMIndexDocumentParams = Omit, 'type'>; - -export interface IndexPrivileges { - has_all_requested: boolean; - index: Record; -} - -interface IndexPrivilegesParams { - index: Array<{ - names: string[] | string; - privileges: string[]; - }>; -} - -export function isApmIndex( - apmIndices: string[], - indexParam: SearchParams['index'] -) { - if (isString(indexParam)) { - return apmIndices.includes(indexParam); - } else if (Array.isArray(indexParam)) { - // return false if at least one of the indices is not an APM index - return indexParam.every((index) => apmIndices.includes(index)); - } - return false; -} - -function addFilterForLegacyData( - apmIndices: string[], - params: ESSearchRequest, - { includeLegacyData = false } = {} -): SearchParams { - // search across all data (including data) - if (includeLegacyData || !isApmIndex(apmIndices, params.index)) { - return params; - } - - const nextParams = merge( - { - body: { - query: { - bool: { - filter: [], - }, - }, - }, - }, - cloneDeep(params) - ); - - // add filter for omitting pre-7.x data - nextParams.body.query.bool.filter.push({ - range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, - }); - - return nextParams; -} - -// add additional params for search (aka: read) requests -function getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - includeLegacyData, -}: { - context: APMRequestHandlerContext; - params: ESSearchRequest; - indices: ApmIndicesConfig; - includeFrozen: boolean; - includeLegacyData?: boolean; -}) { - // Get indices for legacy data filter (only those which apply) - const apmIndices = Object.values( - pickKeys( - indices, - 'apm_oss.sourcemapIndices', - 'apm_oss.errorIndices', - 'apm_oss.onboardingIndices', - 'apm_oss.spanIndices', - 'apm_oss.transactionIndices', - 'apm_oss.metricsIndices' - ) - ); - return { - ...addFilterForLegacyData(apmIndices, params, { includeLegacyData }), // filter out pre-7.0 data - ignore_throttled: !includeFrozen, // whether to query frozen indices or not - }; -} - -interface APMOptions { - includeLegacyData: boolean; -} - -interface ClientCreateOptions { - clientAsInternalUser?: boolean; - indices: ApmIndicesConfig; - includeFrozen: boolean; -} - -export type ESClient = ReturnType; - -function formatObj(obj: Record) { - return JSON.stringify(obj, null, 2); -} - -export function getESClient( - context: APMRequestHandlerContext, - request: KibanaRequest, - { clientAsInternalUser = false, indices, includeFrozen }: ClientCreateOptions -) { - const { - callAsCurrentUser, - callAsInternalUser, - } = context.core.elasticsearch.legacy.client; - - async function callEs(operationName: string, params: Record) { - const startTime = process.hrtime(); - - let res: any; - let esError = null; - try { - res = clientAsInternalUser - ? await callAsInternalUser(operationName, params) - : await callAsCurrentUser(operationName, params); - } catch (e) { - // catch error and throw after outputting debug info - esError = e; - } - - if (context.params.query._debug) { - const highlightColor = esError ? 'bgRed' : 'inverse'; - const diff = process.hrtime(startTime); - const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; - const routeInfo = `${request.route.method.toUpperCase()} ${ - request.route.path - }`; - - console.log( - chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) - ); - - if (operationName === 'search') { - console.log(`GET ${params.index}/_${operationName}`); - console.log(formatObj(params.body)); - } else { - console.log(chalk.bold('ES operation:'), operationName); - - console.log(chalk.bold('ES query:')); - console.log(formatObj(params)); - } - console.log(`\n`); - } - - if (esError) { - throw esError; - } - - return res; - } - - return { - search: async < - TDocument = unknown, - TSearchRequest extends ESSearchRequest = {} - >( - params: TSearchRequest, - apmOptions?: APMOptions - ): Promise> => { - const nextParams = await getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - ...apmOptions, - }); - - return callEs('search', nextParams); - }, - index: (params: APMIndexDocumentParams) => { - return callEs('index', params); - }, - delete: ( - params: Omit - ): Promise => { - return callEs('delete', params); - }, - indicesCreate: (params: IndicesCreateParams) => { - return callEs('indices.create', params); - }, - hasPrivileges: ( - params: IndexPrivilegesParams - ): Promise => { - return callEs('transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: params, - }); - }, - }; -} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 5a4bc62b87486..d8dbd8273f476 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -7,6 +7,8 @@ import { setupRequest } from './setup_request'; import { APMConfig } from '../..'; import { APMRequestHandlerContext } from '../../routes/typings'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; jest.mock('../settings/apm_indices/get_apm_indices', () => ({ getApmIndices: async () => ({ @@ -93,163 +95,175 @@ function getMockRequest() { } describe('setupRequest', () => { - it('should call callWithRequest with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*', body: { foo: 'bar' } } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - it('should call callWithInternalUser with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { internalClient } = await setupRequest(mockContext, mockRequest); - await internalClient.search({ - index: 'apm-*', - body: { foo: 'bar' }, - } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsInternalUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - describe('observer.version_major filter', () => { - describe('if index is apm-*', () => { - it('should merge `observer.version_major` filter with existing boolean filters', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, - }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ + describe('with default args', () => { + it('calls callWithRequest', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { events: [ProcessorEvent.transaction] }, + body: { foo: 'bar' }, + }); + expect( + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', query: { bool: { filter: [ - { term: 'someTerm' }, + { terms: { 'processor.event': ['transaction'] } }, { range: { 'observer.version_major': { gte: 7 } } }, ], }, }, - }); + }, + ignore_throttled: true, }); + }); - it('should add `observer.version_major` filter if none exists', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*' }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }); + it('calls callWithInternalUser', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { internalClient } = await setupRequest(mockContext, mockRequest); + await internalClient.search({ + index: ['apm-*'], + body: { foo: 'bar' }, + } as any); + expect( + mockContext.core.elasticsearch.legacy.client.callAsInternalUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', + }, }); + }); + }); - it('should not add `observer.version_major` filter if `includeLegacyData=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search( - { - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + describe('with a bool filter', () => { + it('adds a range filter for `observer.version_major` to the existing filter', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { term: 'someTerm' }, + { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], }, - { - includeLegacyData: true, - } - ); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { bool: { filter: [{ term: 'someTerm' }] } }, - }); + }, }); }); - it('if index is not an APM index, it should not add `observer.version_major` filter', async () => { + it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: '.ml-*', - body: { - query: { bool: { filter: [{ term: 'someTerm' }] } }, + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search( + { + apm: { + events: [ProcessorEvent.error], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, }, - }); + { + includeLegacyData: true, + } + ); const params = mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock .calls[0][1]; expect(params.body).toEqual({ query: { bool: { - filter: [{ term: 'someTerm' }], + filter: [ + { term: 'someTerm' }, + { + terms: { + [PROCESSOR_EVENT]: ['error'], + }, + }, + ], }, }, }); }); }); +}); - describe('ignore_throttled', () => { - it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { - const { mockContext, mockRequest } = getMockRequest(); +describe('without a bool filter', () => { + it('adds a range filter for `observer.version_major`', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { terms: { [PROCESSOR_EVENT]: ['error'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], + }, + }, + }); + }); +}); - // mock includeFrozen to return false - mockContext.core.uiSettings.client.get.mockResolvedValue(false); +describe('with includeFrozen=false', () => { + it('sets `ignore_throttled=true`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return false + mockContext.core.uiSettings.client.get.mockResolvedValue(false); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(true); + await apmEventClient.search({ + apm: { + events: [], + }, }); - it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(true); + }); +}); - // mock includeFrozen to return true - mockContext.core.uiSettings.client.get.mockResolvedValue(true); +describe('with includeFrozen=true', () => { + it('sets `ignore_throttled=false`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return true + mockContext.core.uiSettings.client.get.mockResolvedValue(true); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(false); + await apmEventClient.search({ + apm: { events: [] }, }); + + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(false); }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6f381d4945ab4..ddad2eb2d22dc 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -13,11 +13,17 @@ import { ApmIndicesConfig, } from '../settings/apm_indices/get_apm_indices'; import { ESFilter } from '../../../typings/elasticsearch'; -import { ESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; import { APMRequestHandlerContext } from '../../routes/typings'; -import { getESClient } from './es_client'; import { ProcessorEvent } from '../../../common/processor_event'; +import { + APMEventClient, + createApmEventClient, +} from './create_es_client/create_apm_event_client'; +import { + APMInternalClient, + createInternalESClient, +} from './create_es_client/create_internal_es_client'; function decodeUiFilters(uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -30,8 +36,8 @@ function decodeUiFilters(uiFiltersEncoded?: string) { // https://github.com/microsoft/TypeScript/issues/34933 export interface Setup { - client: ESClient; - internalClient: ESClient; + apmEventClient: APMEventClient; + internalClient: APMInternalClient; ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; @@ -78,22 +84,19 @@ export async function setupRequest( context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); - const createClientOptions = { - indices, - includeFrozen, - }; - const uiFiltersES = decodeUiFilters(query.uiFilters); const coreSetupRequest = { indices, - client: getESClient(context, request, { - clientAsInternalUser: false, - ...createClientOptions, + apmEventClient: createApmEventClient({ + context, + request, + indices, + options: { includeFrozen }, }), - internalClient: getESClient(context, request, { - clientAsInternalUser: true, - ...createClientOptions, + internalClient: createInternalESClient({ + context, + request, }), ml: getMlSetup(context, request), config, diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index ee03e77de3580..cb30c6c064848 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -11,7 +11,10 @@ import { IIndexPattern, } from '../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; import { APMRequestHandlerContext } from '../../routes/typings'; const cache = new LRU({ @@ -27,7 +30,7 @@ export const getDynamicIndexPattern = async ({ }: { context: APMRequestHandlerContext; indices: ApmIndicesConfig; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; }) => { const patternIndices = getPatternIndices(indices, processorEvent); const indexPatternTitle = patternIndices.join(','); @@ -75,17 +78,17 @@ export const getDynamicIndexPattern = async ({ function getPatternIndices( indices: ApmIndicesConfig, - processorEvent?: ProcessorEvent + processorEvent?: UIProcessorEvent ) { const indexNames = processorEvent ? [processorEvent] - : ['transaction' as const, 'metric' as const, 'error' as const]; + : [ProcessorEvent.transaction, ProcessorEvent.metric, ProcessorEvent.error]; const indicesMap = { - transaction: indices['apm_oss.transactionIndices'], - metric: indices['apm_oss.metricsIndices'], - error: indices['apm_oss.errorIndices'], + [ProcessorEvent.transaction]: indices['apm_oss.transactionIndices'], + [ProcessorEvent.metric]: indices['apm_oss.metricsIndices'], + [ProcessorEvent.error]: indices['apm_oss.errorIndices'], }; - return indexNames.map((name) => indicesMap[name]); + return indexNames.map((name) => indicesMap[name as UIProcessorEvent]); } diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index d8119ac96a536..b88c90a213c67 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`metrics queries with a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -66,11 +71,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -95,12 +95,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -155,11 +159,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -189,12 +188,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -251,11 +254,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -290,12 +288,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -350,11 +352,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -384,12 +381,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -434,11 +435,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -468,12 +464,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -538,11 +538,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -573,12 +568,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -633,11 +632,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -673,12 +667,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -735,11 +733,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -780,12 +773,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -840,11 +837,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -880,12 +872,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -930,11 +926,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -970,12 +961,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -1040,11 +1035,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1064,12 +1054,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -1124,11 +1118,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1153,12 +1142,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -1215,11 +1208,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1249,12 +1237,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -1309,11 +1301,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1338,12 +1325,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -1388,11 +1379,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1417,6 +1403,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 3ed6e4a944b51..e5c573ba1ec02 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -18,8 +18,8 @@ import { } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; -import { getMetricsProjection } from '../../../../../../common/projections/metrics'; -import { mergeProjection } from '../../../../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../../../../projections/metrics'; +import { mergeProjection } from '../../../../../projections/util/merge_projection'; import { AGENT_NAME, LABEL_NAME, @@ -42,7 +42,7 @@ export async function fetchAndTransformGcMetrics({ chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const { bucketSize } = getBucketSize(start, end, 'auto'); @@ -105,7 +105,7 @@ export async function fetchAndTransformGcMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); const { aggregations } = response; diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 895920a9b6c7d..f6e201b395c37 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -5,7 +5,6 @@ */ import { Unionize, Overwrite } from 'utility-types'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange, @@ -14,9 +13,10 @@ import { import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; -import { getMetricsProjection } from '../../../common/projections/metrics'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../projections/metrics'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; @@ -28,7 +28,7 @@ type MetricsAggregationMap = Unionize<{ type MetricAggs = Record; export type GenericMetricsRequest = Overwrite< - ESSearchRequest, + APMEventESSearchRequest, { body: { aggs: { @@ -65,7 +65,7 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const projection = getMetricsProjection({ setup, @@ -91,7 +91,7 @@ export async function fetchAndTransformMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); return transformDataToMetricsChart(response, chartBase); } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 4c4d058c7139d..8a1f3cb0e0149 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -6,10 +6,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { - SERVICE_NAME, - PROCESSOR_EVENT, -} from '../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceCount({ @@ -17,36 +14,27 @@ export async function getServiceCount({ }: { setup: Setup & SetupTimeRange; }) { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { range: rangeFilter(start, end) }, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.transaction, - ProcessorEvent.metric, - ], - }, - }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); return aggregations?.serviceCount.value || 0; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts index 0d1a4274c16dc..116b37a395299 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts @@ -10,7 +10,6 @@ */ import { rangeFilter } from '../../../common/utils/range_filter'; import { Coordinates } from '../../../../observability/public'; -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -21,18 +20,17 @@ export async function getTransactionCoordinates({ setup: Setup & SetupTimeRange; bucketSize: string; }): Promise { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; - const { aggregations } = await client.search({ - index: indices['apm_oss.transactionIndices'], + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { range: rangeFilter(start, end) }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index fc7445ab4a225..66d82b9f88355 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -3,41 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; export async function hasData({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; try { const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, terminateAfter: 1, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response.hits.total.value > 0; } catch (e) { return false; diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 602eb88ba8940..c5264373ea495 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`rum client dashboard queries fetches client metrics 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "backEnd": Object { @@ -34,11 +39,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page load distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "durPercentiles": Object { @@ -101,11 +105,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -126,12 +125,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page view trends 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "pageViews": Object { @@ -154,11 +157,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -179,12 +177,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches rum services 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -206,11 +208,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -231,6 +228,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 8b3f733fc402a..194c136e2b3d0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -45,9 +45,9 @@ export async function getClientMetrics({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { backEnd, domInteractive, pageViews } = response.aggregations!; // Divide by 1000 to convert ms into seconds diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index e847a87264759..2a0c709ea9235 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -57,12 +57,12 @@ export async function getPageLoadDistribution({ }, }); - const { client } = setup; + const { apmEventClient } = setup; const { aggregations, hits: { total }, - } = await client.search(params); + } = await apmEventClient.search(params); if (total.value === 0) { return null; @@ -130,9 +130,9 @@ const getPercentilesDistribution = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDist = aggregations?.loadDistribution.values ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 30b2677d3c217..23169ddaca534 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -56,9 +56,9 @@ export async function getPageViewTrends({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.pageViews.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index ea9d701e64c3d..ffb06e649b9be 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -16,6 +17,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; @@ -53,11 +55,11 @@ export const getPageLoadDistBreakdown = async ( }); const params = mergeProjection(projection, { + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { breakdowns: { terms: { @@ -67,7 +69,7 @@ export const getPageLoadDistBreakdown = async ( aggs: { page_dist: { percentile_ranks: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, values: stepValues, keyed: false, hdr: { @@ -81,9 +83,9 @@ export const getPageLoadDistBreakdown = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDistBreakdowns = aggregations?.breakdowns.buckets; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index 5957a25239307..9bfa109f00faf 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; export async function getRumServices({ setup, @@ -38,9 +38,9 @@ export async function getRumServices({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.services.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index a14affb6eeec5..3681923b484b0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -55,9 +55,9 @@ export async function getVisitorBreakdown({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { browsers, os, devices } = response.aggregations!; return { diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 08c8aba5f0207..14047f4bacea9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - PROCESSOR_EVENT, - TRACE_ID, -} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; import { ConnectionNode, ExternalConnectionNode, @@ -18,23 +16,17 @@ export async function fetchServicePathsFromTraceIds( setup: Setup, traceIds: string[] ) { - const { indices, client } = setup; + const { apmEventClient } = setup; const serviceMapParams = { - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ - { - terms: { - [PROCESSOR_EVENT]: ['span', 'transaction'], - }, - }, { terms: { [TRACE_ID]: traceIds, @@ -212,7 +204,7 @@ export async function fetchServicePathsFromTraceIds( }, }; - const serviceMapFromTraceIdsScriptResponse = await client.search( + const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search( serviceMapParams ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index cd125f944f8a5..b162c3b61d928 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -10,8 +10,8 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getServicesProjection } from '../../../common/projections/services'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServicesProjection } from '../../projections/services'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; @@ -118,9 +118,9 @@ async function getServicesData(options: IEnvOptions) { }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); return ( response.aggregations?.services.buckets.map((bucket) => { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index 1e0d001340edf..d1c99d778c8f0 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -12,7 +12,7 @@ describe('getServiceMapServiceNodeInfo', () => { describe('with no results', () => { it('returns null data', async () => { const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 0 } }, @@ -49,7 +49,7 @@ describe('getServiceMapServiceNodeInfo', () => { }); const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 1 } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 0f7136d6d74a4..330d38739a063 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -6,13 +6,12 @@ import { UIFilters } from '../../../typings/ui_filters'; import { + SERVICE_NAME, + TRANSACTION_DURATION, TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -109,17 +108,18 @@ async function getTransactionStats({ avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }> { - const { indices, client } = setup; + const { apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ ...filter, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { terms: { [TRANSACTION_TYPE]: [ @@ -135,8 +135,9 @@ async function getTransactionStats({ aggs: { duration: { avg: { field: TRANSACTION_DURATION } } }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); const docCount = response.hits.total.value; + return { avgTransactionDuration: response.aggregations?.duration.value ?? null, avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, @@ -147,18 +148,17 @@ async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { - const { indices, client } = setup; + const { apmEventClient } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, - { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }, - ]), + filter: [...filter, { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }], }, }, aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } }, @@ -172,17 +172,19 @@ async function getMemoryStats({ setup, filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { - const { client, indices } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const { apmEventClient } = setup; + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: 'metric' } }, + filter: [ + ...filter, { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ]), + ], }, }, aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 11c3a00f32980..d6d681f24ab85 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq, take, sortBy } from 'lodash'; +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, TRACE_ID, @@ -26,18 +26,13 @@ export async function getTraceSampleIds({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices, config } = setup; + const { start, end, apmEventClient, config } = setup; const rangeQuery = { range: rangeFilter(start, end) }; const query = { bool: { filter: [ - { - term: { - [PROCESSOR_EVENT]: 'span', - }, - }, { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE, @@ -67,7 +62,9 @@ export async function getTraceSampleIds({ const samplerShardSize = traceIdBucketSize * 10; const params = { - index: [indices['apm_oss.spanIndices']], + apm: { + events: [ProcessorEvent.span], + }, body: { size: 0, query, @@ -126,9 +123,7 @@ export async function getTraceSampleIds({ }, }; - const tracesSampleResponse = await client.search( - params - ); + const tracesSampleResponse = await apmEventClient.search(params); // make sure at least one trace per composite/connection bucket // is queried diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index 3935ecda42db9..87aca0d056909 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`service node queries fetches metadata for a service node 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -30,11 +35,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches metadata for unidentified service nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -93,11 +97,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches services nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nodes": Object { @@ -174,11 +177,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -197,6 +195,5 @@ Object { }, }, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index de66c242815a4..a83aba192dba9 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -9,8 +9,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { METRIC_PROCESS_CPU_PERCENT, @@ -26,7 +26,7 @@ const getServiceNodes = async ({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) => { - const { client } = setup; + const { apmEventClient } = setup; const projection = getServiceNodesProjection({ setup, serviceName }); @@ -66,7 +66,7 @@ const getServiceNodes = async ({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); if (!response.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 0fc1f89a3723b..ca86c1d93fa6e 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -2,48 +2,32 @@ exports[`services queries fetches the agent status 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "metric", + "sourcemap", + "transaction", + ], + }, "body": Object { - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "error", - "metric", - "sourcemap", - "transaction", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; exports[`services queries fetches the legacy data status 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "observer.version_major": Object { @@ -56,13 +40,19 @@ Object { }, "size": 0, }, - "index": "myIndex", "terminateAfter": 1, } `; exports[`services queries fetches the service agent name 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "transaction", + "metric", + ], + }, "body": Object { "aggs": Object { "agents": Object { @@ -80,15 +70,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "error", - "transaction", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -103,11 +84,6 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; @@ -115,6 +91,11 @@ Object { exports[`services queries fetches the service items 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -148,20 +129,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", - "size": 0, }, Object { + "apm": Object { + "events": Array [ + "metric", + "error", + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -198,27 +179,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "metric", - "error", - "transaction", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -245,19 +217,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -284,19 +255,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "metric", + "transaction", + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -330,31 +302,22 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, ] `; exports[`services queries fetches the service transaction types 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "types": Object { @@ -372,13 +335,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -393,6 +349,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 6a8aaf8dca8a6..ad3f47d443b87 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SetupTimeRange, Setup } from '../../helpers/setup_request'; import { ESFilter } from '../../../../typings/elasticsearch'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; @@ -24,23 +24,24 @@ export async function getDerivedServiceAnnotations({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, ...getEnvironmentUiFilterES(environment), ]; const versions = ( - await client.search({ - index: indices['apm_oss.transactionIndices'], + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ range: rangeFilter(start, end) }), + filter: [...filter, { range: rangeFilter(start, end) }], }, }, aggs: { @@ -59,17 +60,15 @@ export async function getDerivedServiceAnnotations({ } const annotations = await Promise.all( versions.map(async (version) => { - const response = await client.search({ - index: indices['apm_oss.transactionIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ - term: { - [SERVICE_VERSION]: version, - }, - }), + filter: [...filter, { term: { [SERVICE_VERSION]: version } }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index 8d75d746c7fca..a95c27df0e502 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, AGENT_NAME, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -15,24 +15,23 @@ export async function getServiceAgentName( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { - terms: { [PROCESSOR_EVENT]: ['error', 'transaction', 'metric'] }, - }, { range: rangeFilter(start, end) }, ], }, @@ -45,7 +44,7 @@ export async function getServiceAgentName( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; return { agentName }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index c2d9fa6c1df39..fca472b0ce8c2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -14,8 +14,8 @@ import { CONTAINER_ID, } from '../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; export async function getServiceNodeMetadata({ serviceName, @@ -26,7 +26,7 @@ export async function getServiceNodeMetadata({ serviceNodeName: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; const query = mergeProjection( getServiceNodesProjection({ @@ -55,7 +55,7 @@ export async function getServiceNodeMetadata({ } ); - const response = await client.search(query); + const response = await apmEventClient.search(query); return { host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index d88be4055dc21..6c6e03ab0b46f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; @@ -15,17 +15,18 @@ export async function getServiceTransactionTypes( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, { range: rangeFilter(start, end) }, ], }, @@ -38,7 +39,7 @@ export async function getServiceTransactionTypes( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const transactionTypes = aggregations?.types.buckets.map((bucket) => bucket.key as string) || []; return { transactionTypes }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts index dde726c51393f..1be95967cb47a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts @@ -4,33 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - OBSERVER_VERSION_MAJOR, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { OBSERVER_VERSION_MAJOR } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; // returns true if 6.x data is found export async function getLegacyDataStatus(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, - { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }, - ], + filter: [{ range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }], }, }, }, }; - const resp = await client.search(params, { includeLegacyData: true }); + const resp = await apmEventClient.search(params, { includeLegacyData: true }); const hasLegacyData = resp.hits.total.value > 0; return hasLegacyData; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 14772e77fe1c2..d888b43b63fac 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -10,7 +10,7 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; import { getTransactionDurationAverages, getAgentNames, @@ -25,7 +25,7 @@ export type ServicesItemsProjection = ReturnType; export async function getServicesItems(setup: ServicesItemsSetup) { const params = { - projection: getServicesProjection({ setup, noEvents: true }), + projection: getServicesProjection({ setup }), setup, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index de699028f5675..ddce3b667a603 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -5,12 +5,11 @@ */ import { - PROCESSOR_EVENT, TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, } from '../../../../common/elasticsearch_fieldnames'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ServicesItemsSetup, @@ -31,22 +30,15 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; + const { apmEventClient } = setup; - const response = await client.search( + const response = await apmEventClient.search( mergeProjection(projection, { - size: 0, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { - query: { - bool: { - filter: projection.body.query.bool.filter.concat({ - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }), - }, - }, + size: 0, aggs: { services: { terms: { @@ -82,32 +74,18 @@ export const getAgentNames = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.error, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.metric, - ProcessorEvent.error, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -136,11 +114,7 @@ export const getAgentNames = async ({ return aggregations.services.buckets.map((bucket) => ({ serviceName: bucket.key as string, - agentName: (bucket.agent_name.hits.hits[0]?._source as { - agent: { - name: string; - }; - }).agent.name, + agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, })); }; @@ -148,24 +122,14 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -199,24 +163,14 @@ export const getErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.error, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -250,32 +204,18 @@ export const getEnvironments = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.transaction, + ProcessorEvent.error, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts index 42f53fc93fa60..eed9f2588152d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts @@ -4,43 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; // Note: this logic is duplicated in tutorials/apm/envs/on_prem export async function hasHistoricalAgentData(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - indices['apm_oss.sourcemapIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.metric, + ProcessorEvent.sourcemap, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - 'error', - 'metric', - 'sourcemap', - 'transaction', - ], - }, - }, - ], - }, - }, }, }; - const resp = await client.search(params); - const hasHistorialAgentData = resp.hits.total.value > 0; - return hasHistorialAgentData; + const resp = await apmEventClient.search(params); + return resp.hits.total.value > 0; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 24a1840bc0ab8..2b465a0f87475 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -115,6 +115,13 @@ Object { exports[`agent configuration queries getServiceNames fetches service names 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -124,28 +131,8 @@ Object { }, }, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 4d61e1e9ae284..86aeb95e165a0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -10,7 +10,7 @@ import { AgentConfiguration, AgentConfigurationIntake, } from '../../../../common/agent_configuration/configuration_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateConfiguration({ configurationId, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index 39674ee57abf6..9f0e65d492a8f 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; export async function getAgentNameByService({ @@ -18,25 +16,22 @@ export async function getAgentNameByService({ serviceName: string; setup: Setup; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - { term: { [SERVICE_NAME]: serviceName } }, - ], + filter: [{ term: { [SERVICE_NAME]: serviceName } }], }, }, aggs: { @@ -47,7 +42,7 @@ export async function getAgentNameByService({ }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agent_names.buckets[0]?.key; return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 068bb30ddcf79..8b6c1d82beab0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -4,37 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; export async function getServiceNames({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -46,7 +37,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) { }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const serviceNames = resp.aggregations?.services.buckets .map((bucket) => bucket.key as string) diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap index a91641b592526..0649c8c38d29a 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap @@ -2,15 +2,15 @@ exports[`custom link get transaction fetches with all filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "terms": Object { "service.name": Array [ @@ -43,7 +43,6 @@ Object { }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } @@ -51,20 +50,18 @@ Object { exports[`custom link get transaction fetches without filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { - "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, - ], + "filter": Array [], }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts index 16a694c04c485..48b115619283c 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts @@ -8,9 +8,9 @@ import { CustomLink, CustomLinkES, } from '../../../../common/custom_link/custom_link_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; import { Setup } from '../../helpers/setup_request'; import { toESFormat } from './helper'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateCustomLink({ customLinkId, diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts index e3becc040580f..9bf489e768a4b 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import * as t from 'io-ts'; -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { filterOptionsRt } from './custom_link_types'; @@ -18,7 +16,7 @@ export async function getTransaction({ setup: Setup; filters?: t.TypeOf; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const esFilters = Object.entries(filters) // loops through the filters splitting the value by comma and removing white spaces @@ -32,19 +30,18 @@ export async function getTransaction({ const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, size: 1, body: { query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...esFilters, - ], + filter: esFilters, }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index 0a9f9d38b2be7..3c521839b587e 100644 --- a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`trace queries fetches a trace 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "by_transaction_id": Object { @@ -20,11 +25,6 @@ Object { "trace.id": "foo", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -48,6 +48,5 @@ Object { }, "size": "myIndex", }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index f9374558dfeeb..17f9743ae9f00 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, TRANSACTION_DURATION, @@ -13,8 +13,6 @@ import { TRANSACTION_ID, ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; -import { Span } from '../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -28,19 +26,20 @@ export async function getTraceItems( traceId: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, config, indices } = setup; + const { start, end, apmEventClient, config } = setup; const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; const excludedLogLevels = ['debug', 'info', 'warning']; - const errorResponsePromise = client.search({ - index: indices['apm_oss.errorIndices'], + const errorResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ], must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, @@ -59,18 +58,16 @@ export async function getTraceItems( }, }); - const traceResponsePromise = client.search({ - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + const traceResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) }, ], should: { @@ -91,22 +88,17 @@ export async function getTraceItems( // explicit intermediary types to avoid TS "excessively deep" error PromiseValueType, PromiseValueType - // @ts-ignore ] = await Promise.all([errorResponsePromise, traceResponsePromise]); const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const items = (traceResponse.hits.hits as Array<{ - _source: Transaction | Span; - }>).map((hit) => hit._source); + const items = traceResponse.hits.hits.map((hit) => hit._source); const errorFrequencies: { errorsPerTransaction: ErrorsPerTransaction; errorDocs: APMError[]; } = { - errorDocs: errorResponse.hits.hits.map( - ({ _source }) => _source as APMError - ), + errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), errorsPerTransaction: errorResponse.aggregations?.by_transaction_id.buckets.reduce( (acc, current) => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index deca46f4ebd0c..0ea7bcf7ce8ab 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -3,6 +3,11 @@ exports[`transaction group queries fetches top traces 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -46,11 +51,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -84,10 +84,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -131,11 +135,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -152,10 +151,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -199,11 +202,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -220,7 +218,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] @@ -229,6 +226,11 @@ Array [ exports[`transaction group queries fetches top transactions 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -257,11 +259,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -298,10 +295,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -330,11 +331,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -354,10 +350,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -386,11 +386,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -410,10 +405,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -448,11 +447,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -472,7 +466,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 73bf1d01924e7..b06d1a8af3bc5 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -7,13 +7,12 @@ import { take, sortBy } from 'lodash'; import { Unionize } from 'utility-types'; import moment from 'moment'; import { joinByKey } from '../../../common/utils/join_by_key'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getTransactionGroupsProjection } from '../../projections/transaction_groups'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Transaction } from '../../../typings/es_schemas/ui/transaction'; @@ -45,7 +44,9 @@ export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export type TransactionGroupRequestBase = ESSearchRequest & { +export type TransactionGroupRequestBase = ReturnType< + typeof getTransactionGroupsProjection +> & { body: { aggs: { transaction_groups: Unionize< diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 6a1ee8daad7c7..8fb2ceb30db85 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -5,7 +5,6 @@ */ import { mean } from 'lodash'; import { - PROCESSOR_EVENT, HTTP_RESPONSE_STATUS_CODE, TRANSACTION_NAME, TRANSACTION_TYPE, @@ -31,7 +30,7 @@ export async function getErrorRate({ transactionName?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const transactionNamefilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -42,7 +41,6 @@ export async function getErrorRate({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: rangeFilter(start, end) }, { exists: { field: HTTP_RESPONSE_STATUS_CODE } }, ...transactionNamefilter, @@ -51,7 +49,9 @@ export async function getErrorRate({ ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -68,7 +68,7 @@ export async function getErrorRate({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const noHits = resp.hits.total.value === 0; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 59fb370113ec2..7d45f39e08a83 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -5,7 +5,6 @@ */ import { merge } from 'lodash'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { TRANSACTION_SAMPLED, TRANSACTION_DURATION, @@ -52,7 +51,7 @@ export async function getSamples({ request, setup }: MetricParams) { { '@timestamp': { order: 'desc' as const } }, ]; - const response = await setup.client.search({ + const response = await setup.apmEventClient.search({ ...params, body: { ...params.body, @@ -73,7 +72,7 @@ export async function getSamples({ request, setup }: MetricParams) { return { key: bucket.key as BucketKey, count: bucket.doc_count, - sample: bucket.sample.hits.hits[0]._source as Transaction, + sample: bucket.sample.hits.hits[0]._source, }; }); } @@ -87,7 +86,7 @@ export async function getAverages({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -108,7 +107,7 @@ export async function getSums({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -131,7 +130,7 @@ export async function getPercentiles({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index cc5900919f829..9bc4b1d69d9ac 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -2,15 +2,15 @@ exports[`transaction queries fetches a transaction 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.id": "foo", @@ -35,12 +35,16 @@ Object { }, "size": 1, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -146,11 +150,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -170,12 +169,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions for a transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -281,11 +284,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -310,12 +308,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -376,11 +378,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -405,12 +402,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -471,11 +472,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -505,12 +501,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -571,11 +571,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -610,12 +605,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "stats": Object { @@ -632,11 +631,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "baz", @@ -666,6 +660,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts index d8175a34ceb9f..278819ea20a83 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts @@ -15,7 +15,7 @@ describe('fetcher', () => { it('performs a search', async () => { const search = jest.fn(); const setup = ({ - client: { search }, + apmEventClient: { search }, indices: {}, uiFiltersES: [], } as unknown) as Setup & SetupTimeRange & SetupUIFilters; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index b4d98ec41fc2d..f68082dfaa1e1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -7,7 +7,6 @@ import { ESFilter } from '../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, USER_AGENT_NAME, @@ -23,7 +22,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { - const { end, client, indices, start, uiFiltersES } = options.setup; + const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; const { intervalString } = getBucketSize(start, end, 'auto'); @@ -32,7 +31,6 @@ export function fetcher(options: Options) { : []; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { range: rangeFilter(start, end) }, @@ -41,7 +39,9 @@ export function fetcher(options: Options) { ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -80,5 +80,5 @@ export function fetcher(options: Options) { }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index ea6213f64ee36..9bb42d2fa7aad 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { CLIENT_GEO_COUNTRY_ISO_CODE, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_TYPE, @@ -29,12 +29,14 @@ export async function getTransactionAvgDurationByCountry({ serviceName: string; transactionName?: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] : []; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { @@ -42,7 +44,6 @@ export async function getTransactionAvgDurationByCountry({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, ...transactionNameFilter, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, { range: rangeFilter(start, end) }, @@ -66,7 +67,7 @@ export async function getTransactionAvgDurationByCountry({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); if (!resp.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 85d4eab448c72..3c1618ed7715f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -26,7 +26,7 @@ function getMockSetup(esResponse: any) { return { start: 0, end: 500000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 3c48c14c2a471..7248399d1f93f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, SPAN_SUBTYPE, @@ -13,7 +14,6 @@ import { TRANSACTION_TYPE, TRANSACTION_NAME, TRANSACTION_BREAKDOWN_COUNT, - PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { Setup, @@ -36,7 +36,7 @@ export async function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const subAggs = { sum_all_self_times: { @@ -82,7 +82,6 @@ export async function getTransactionBreakdown({ const filters = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ]; @@ -92,7 +91,9 @@ export async function getTransactionBreakdown({ } const params = { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { @@ -110,7 +111,7 @@ export async function getTransactionBreakdown({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const formatBucket = ( aggs: diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 25ebb15fd73e8..7bc60a7fc7f1a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should call client with correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -64,11 +69,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -98,7 +98,6 @@ Array [ }, "size": 0, }, - "index": "myIndex", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index fb357040f5781..09e1287f032f5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { ESResponse, timeseriesFetcher } from './fetcher'; import { APMConfig } from '../../../../../server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let res: ESResponse; @@ -21,7 +21,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, @@ -54,15 +54,7 @@ describe('timeseriesFetcher', () => { it('should restrict results to only transaction documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([ - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - } as any, - ]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.transaction]); }); it('should return correct response', () => { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 8e19af926ce02..1498c22e327d6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESFilter } from '../../../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -34,11 +34,10 @@ export function timeseriesFetcher({ transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const { intervalString } = getBucketSize(start, end, 'auto'); const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -54,7 +53,9 @@ export function timeseriesFetcher({ } const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter } }, @@ -95,5 +96,5 @@ export function timeseriesFetcher({ }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 3f8bf635712be..bfe72bf7c00f9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { ProcessorEvent } from '../../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRACE_ID, TRANSACTION_DURATION, @@ -32,17 +31,18 @@ export async function bucketFetcher( bucketSize: number, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { range: rangeFilter(start, end) }, @@ -85,7 +85,7 @@ export async function bucketFetcher( }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 8289113fddae9..139dac3df1171 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -23,17 +23,18 @@ export async function getDistributionMax( transactionType: string, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { @@ -59,6 +60,6 @@ export async function getDistributionMax( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.aggregations ? resp.aggregations.stats.max : null; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index a7de93a3bf650..9aa1a8f4de87f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, @@ -27,16 +25,17 @@ export async function getTransaction({ traceId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const params = { - index: indices['apm_oss.transactionIndices'], + const resp = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 1, query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, { range: rangeFilter(start, end) }, @@ -44,8 +43,7 @@ export async function getTransaction({ }, }, }, - }; + }); - const resp = await client.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts index ad4f58d85d188..8ba61c6c726a4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -17,9 +15,12 @@ export async function getRootTransactionByTraceId( traceId: string, setup: Setup ) { - const { client, indices } = setup; + const { apmEventClient } = setup; + const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 1, query: { @@ -35,16 +36,13 @@ export async function getRootTransactionByTraceId( }, }, ], - filter: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ], + filter: [{ term: { [TRACE_ID]: traceId } }], }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return { transaction: resp.hits.hits[0]?._source, }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap index 30e75f46ad5e7..d94b766aee6a8 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`ui filter queries fetches environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -14,15 +21,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -42,16 +40,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`ui filter queries fetches environments without a service name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -64,15 +64,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -87,10 +78,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 3fca30634be6a..98f00bf8e6555 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -18,12 +18,9 @@ export async function getEnvironments( setup: Setup & SetupTimeRange, serviceName?: string ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const filter: ESFilter[] = [ - { terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }, - { range: rangeFilter(start, end) }, - ]; + const filter: ESFilter[] = [{ range: rangeFilter(start, end) }]; if (serviceName) { filter.push({ @@ -32,11 +29,13 @@ export async function getEnvironments( } const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, body: { size: 0, query: { @@ -55,7 +54,7 @@ export async function getEnvironments( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const aggs = resp.aggregations; const environmentsBuckets = aggs?.environments.buckets || []; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap index e6b6a9a52adfe..5f38432719280 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "by_terms": Object { @@ -28,15 +35,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -56,10 +54,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts index e892284fd87cd..cfbd79d37c041 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -5,8 +5,8 @@ */ import { omit } from 'lodash'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; -import { Projection } from '../../../../common/projections/typings'; +import { mergeProjection } from '../../../projections/util/merge_projection'; +import { Projection } from '../../../projections/typings'; import { UIFilters } from '../../../../typings/ui_filters'; import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; import { localUIFilters, LocalUIFilterName } from './config'; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 3833b93c8d1f7..12c02679d0859 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -5,7 +5,7 @@ */ import { cloneDeep, orderBy } from 'lodash'; import { UIFilters } from '../../../../typings/ui_filters'; -import { Projection } from '../../../../common/projections/typings'; +import { Projection } from '../../../projections/typings'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; import { Setup } from '../../helpers/setup_request'; @@ -26,7 +26,7 @@ export async function getLocalUIFilters({ uiFilters: UIFilters; localFilterNames: LocalUIFilterName[]; }) { - const { client } = setup; + const { apmEventClient } = setup; const projectionWithoutAggs = cloneDeep(projection); @@ -40,7 +40,7 @@ export async function getLocalUIFilters({ localUIFilterName: name, }); - const response = await client.search(query); + const response = await apmEventClient.search(query); const filter = localUIFilters[name]; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index ac61910968850..92ee67de49314 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -9,7 +9,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../../../public/utils/testHelpers'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; describe('local ui filter queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/common/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts similarity index 70% rename from x-pack/plugins/apm/common/projections/errors.ts rename to x-pack/plugins/apm/server/projections/errors.ts index 390a8a0968102..49a0e9f479d26 100644 --- a/x-pack/plugins/apm/common/projections/errors.ts +++ b/x-pack/plugins/apm/server/projections/errors.ts @@ -8,15 +8,13 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, ERROR_GROUP_ID, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getErrorGroupsProjection({ setup, @@ -25,16 +23,17 @@ export function getErrorGroupsProjection({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; return { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ], diff --git a/x-pack/plugins/apm/common/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts similarity index 72% rename from x-pack/plugins/apm/common/projections/metrics.ts rename to x-pack/plugins/apm/server/projections/metrics.ts index 45998bfe82e96..eb80a6bc73248 100644 --- a/x-pack/plugins/apm/common/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, - PROCESSOR_EVENT, SERVICE_NODE_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; -import { SERVICE_NODE_NAME_MISSING } from '../service_nodes'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes'; +import { ProcessorEvent } from '../../common/processor_event'; function getServiceNodeNameFilters(serviceNodeName?: string) { if (!serviceNodeName) { @@ -40,18 +38,19 @@ export function getMetricsProjection({ serviceName: string; serviceNodeName?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...getServiceNodeNameFilters(serviceNodeName), ...uiFiltersES, ]; return { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { diff --git a/x-pack/plugins/apm/common/projections/rum_overview.ts b/x-pack/plugins/apm/server/projections/rum_overview.ts similarity index 71% rename from x-pack/plugins/apm/common/projections/rum_overview.ts rename to x-pack/plugins/apm/server/projections/rum_overview.ts index b1218546d09ff..4588ec2a0451f 100644 --- a/x-pack/plugins/apm/common/projections/rum_overview.ts +++ b/x-pack/plugins/apm/server/projections/rum_overview.ts @@ -8,22 +8,21 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames'; -import { rangeFilter } from '../utils/range_filter'; +import { TRANSACTION_TYPE } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getRumOverviewProjection({ setup, }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: 'page-load' } }, { // Adding this filter to cater for some inconsistent rum data @@ -36,7 +35,9 @@ export function getRumOverviewProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/service_nodes.ts b/x-pack/plugins/apm/server/projections/service_nodes.ts similarity index 88% rename from x-pack/plugins/apm/common/projections/service_nodes.ts rename to x-pack/plugins/apm/server/projections/service_nodes.ts index 1bc68f51a26ed..87fe815a12d0d 100644 --- a/x-pack/plugins/apm/common/projections/service_nodes.ts +++ b/x-pack/plugins/apm/server/projections/service_nodes.ts @@ -8,9 +8,8 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { SERVICE_NODE_NAME } from '../elasticsearch_fieldnames'; +import { SERVICE_NODE_NAME } from '../../common/elasticsearch_fieldnames'; import { mergeProjection } from './util/merge_projection'; import { getMetricsProjection } from './metrics'; diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts new file mode 100644 index 0000000000000..18fa79f31d6f3 --- /dev/null +++ b/x-pack/plugins/apm/server/projections/services.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 { + Setup, + SetupUIFilters, + SetupTimeRange, +} from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; + +export function getServicesProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES } = setup; + + return { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [{ range: rangeFilter(start, end) }, ...uiFiltersES], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + }, + }, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/common/projections/transaction_groups.ts b/x-pack/plugins/apm/server/projections/transaction_groups.ts similarity index 86% rename from x-pack/plugins/apm/common/projections/transaction_groups.ts rename to x-pack/plugins/apm/server/projections/transaction_groups.ts index 1708d89aad4ec..8aa085cccf82a 100644 --- a/x-pack/plugins/apm/common/projections/transaction_groups.ts +++ b/x-pack/plugins/apm/server/projections/transaction_groups.ts @@ -8,10 +8,11 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { + TRANSACTION_NAME, + PARENT_ID, +} from '../../common/elasticsearch_fieldnames'; import { Options } from '../../server/lib/transaction_groups/fetcher'; import { getTransactionsProjection } from './transactions'; import { mergeProjection } from './util/merge_projection'; diff --git a/x-pack/plugins/apm/common/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts similarity index 76% rename from x-pack/plugins/apm/common/projections/transactions.ts rename to x-pack/plugins/apm/server/projections/transactions.ts index b6cd73ca9aaad..f428a76a8b0cb 100644 --- a/x-pack/plugins/apm/common/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, TRANSACTION_TYPE, - PROCESSOR_EVENT, TRANSACTION_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getTransactionsProjection({ setup, @@ -30,7 +28,7 @@ export function getTransactionsProjection({ transactionName?: string; transactionType?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -45,7 +43,6 @@ export function getTransactionsProjection({ const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, ...transactionNameFilter, ...transactionTypeFilter, ...serviceNameFilter, @@ -54,7 +51,9 @@ export function getTransactionsProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts similarity index 56% rename from x-pack/plugins/apm/common/projections/typings.ts rename to x-pack/plugins/apm/server/projections/typings.ts index 693795b09e1d0..77a5beaf54605 100644 --- a/x-pack/plugins/apm/common/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchRequest, ESSearchBody } from '../../typings/elasticsearch'; +import { ESSearchBody } from '../../typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap, } from '../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; -export type Projection = Omit & { +export type Projection = Omit & { body: Omit & { aggs?: { [key: string]: { @@ -20,14 +21,3 @@ export type Projection = Omit & { }; }; }; - -export enum PROJECTION { - SERVICES = 'services', - TRANSACTION_GROUPS = 'transactionGroups', - TRACES = 'traces', - TRANSACTIONS = 'transactions', - METRICS = 'metrics', - ERROR_GROUPS = 'errorGroups', - SERVICE_NODES = 'serviceNodes', - RUM_OVERVIEW = 'rumOverview', -} diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts similarity index 73% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts index 33727fcb9c735..aa02c8898d218 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts @@ -10,10 +10,19 @@ describe('mergeProjection', () => { it('overrides arrays', () => { expect( mergeProjection( - { body: { query: { bool: { must: [{ terms: ['a'] }] } } } }, - { body: { query: { bool: { must: [{ term: 'b' }] } } } } + { + apm: { events: [] }, + body: { query: { bool: { must: [{ terms: ['a'] }] } } }, + }, + { + apm: { events: [] }, + body: { query: { bool: { must: [{ term: 'b' }] } } }, + } ) ).toEqual({ + apm: { + events: [], + }, body: { query: { bool: { @@ -32,8 +41,11 @@ describe('mergeProjection', () => { const termsAgg = { terms: { field: 'bar' } }; expect( mergeProjection( - { body: { query: {}, aggs: { foo: termsAgg } } }, + { apm: { events: [] }, body: { query: {}, aggs: { foo: termsAgg } } }, { + apm: { + events: [], + }, body: { aggs: { foo: { ...termsAgg, aggs: { bar: { terms: { field: 'baz' } } } }, @@ -42,6 +54,9 @@ describe('mergeProjection', () => { } ) ).toEqual({ + apm: { + events: [], + }, body: { query: {}, aggs: { diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts similarity index 82% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index 9dc1c815bf169..ea7267dd337c2 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -6,15 +6,13 @@ import { mergeWith, isPlainObject, cloneDeep } from 'lodash'; import { DeepPartial } from 'utility-types'; import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; -import { - ESSearchRequest, - ESSearchBody, -} from '../../../../typings/elasticsearch'; +import { ESSearchBody } from '../../../../typings/elasticsearch'; import { Projection } from '../../typings'; +import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; type PlainObject = Record; -type SourceProjection = Omit, 'body'> & { +type SourceProjection = Omit, 'body'> & { body: Omit, 'aggs'> & { aggs?: AggregationInputMap; }; diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index a47d72751dfc4..864f5033c9d62 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -13,23 +13,23 @@ import { SetupTimeRange, } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { Projection } from '../../common/projections/typings'; +import { Projection } from '../projections/typings'; import { localUIFilterNames, LocalUIFilterName, } from '../lib/ui_filters/local_ui_filters/config'; import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { getServicesProjection } from '../../common/projections/services'; -import { getTransactionGroupsProjection } from '../../common/projections/transaction_groups'; -import { getMetricsProjection } from '../../common/projections/metrics'; -import { getErrorGroupsProjection } from '../../common/projections/errors'; -import { getTransactionsProjection } from '../../common/projections/transactions'; +import { getServicesProjection } from '../projections/services'; +import { getTransactionGroupsProjection } from '../projections/transaction_groups'; +import { getMetricsProjection } from '../projections/metrics'; +import { getErrorGroupsProjection } from '../projections/errors'; +import { getTransactionsProjection } from '../projections/transactions'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { jsonRt } from '../../common/runtime_types/json_rt'; -import { getServiceNodesProjection } from '../../common/projections/service_nodes'; -import { getRumOverviewProjection } from '../../common/projections/rum_overview'; +import { getServiceNodesProjection } from '../projections/service_nodes'; +import { getRumOverviewProjection } from '../projections/rum_overview'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index a090f09a76ea2..23fbc912d739a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -7,7 +7,6 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index 5ac91bec849c2..2e053f9084296 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -6,7 +6,6 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/__snapshots__/advanced_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/__snapshots__/advanced_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/advanced_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/advanced_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/dropdown_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/dropdown_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/time_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/time_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/__snapshots__/metric.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/__snapshots__/metric.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/metric.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/metric.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/date_format.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/date_format.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/date_format.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/date_format.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/number_format.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/number_format.stories.tsx diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 9b1d60f38eb5e..71e3386d821f1 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: { @@ -913,6 +913,13 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.toolbar.workpadManagerCloseButtonLabel', { defaultMessage: 'Close', }), + getErrorMessage: (message: string) => + i18n.translate('xpack.canvas.toolbar.errorMessage', { + defaultMessage: 'TOOLBAR ERROR: {message}', + values: { + message, + }, + }), }, ToolbarTray: { getCloseTrayAriaLabel: () => @@ -1301,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/public/components/asset_manager/__examples__/provider.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx deleted file mode 100644 index 1cd7562b59c47..0000000000000 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx +++ /dev/null @@ -1,110 +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. - */ - -/* eslint-disable no-console */ - -/* - This Provider is temporary. See https://github.com/elastic/kibana/pull/69357 -*/ - -import React, { FC } from 'react'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; -import thunkMiddleware from 'redux-thunk'; -import { Provider as ReduxProvider } from 'react-redux'; - -// @ts-expect-error untyped local -import { appReady } from '../../../../public/state/middleware/app_ready'; -// @ts-expect-error untyped local -import { resolvedArgs } from '../../../../public/state/middleware/resolved_args'; - -// @ts-expect-error untyped local -import { getRootReducer } from '../../../../public/state/reducers'; - -// @ts-expect-error Untyped local -import { getDefaultWorkpad } from '../../../../public/state/defaults'; -import { State, AssetType } from '../../../../types'; - -export const AIRPLANE: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'airplane', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const MARKER: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'marker', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const state: State = { - app: { - basePath: '/', - ready: true, - serverFunctions: [], - }, - assets: { - AIRPLANE, - MARKER, - }, - transient: { - canUserWrite: true, - zoomScale: 1, - elementStats: { - total: 0, - ready: 0, - pending: 0, - error: 0, - }, - inFlight: false, - fullScreen: false, - selectedTopLevelNodes: [], - resolvedArgs: {}, - refresh: { - interval: 0, - }, - autoplay: { - enabled: false, - interval: 10000, - }, - }, - persistent: { - schemaVersion: 2, - workpad: getDefaultWorkpad(), - }, -}; - -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { image } from '../../../../canvas_plugin_src/elements/image'; -elementsRegistry.register(image); - -export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( - action -) => { - const previousState = store.getState(); - const returnValue = dispatch(action); - const newState = store.getState(); - - console.group(action.type || '(thunk)'); - console.log('Previous State', previousState); - console.log('New State', newState); - console.groupEnd(); - - return returnValue; -}; - -export const Provider: FC = ({ children }) => { - const middleware = applyMiddleware(thunkMiddleware); - const reducer = getRootReducer(state); - const store = createStore(reducer, state, middleware); - store.dispatch = patchDispatch(store, store.dispatch); - - return {children}; -}; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx new file mode 100644 index 0000000000000..0b99bbce50288 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; +import { Asset, AssetComponent } from '../'; +import { AIRPLANE, MARKER, assets } from './assets'; + +storiesOf('components/Assets/Asset', module) + .addDecorator((story) =>
{story()}
) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: Asset', () => { + return ; + }) + .add('airplane', () => ( + + )) + .add('marker', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx similarity index 50% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx index 1434ef60cf0d8..673c66734b39a 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx @@ -4,35 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; -import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; import { AssetManager, AssetManagerComponent } from '../'; - -import { Provider, AIRPLANE, MARKER } from './provider'; +import { assets } from './assets'; storiesOf('components/Assets/AssetManager', module) - .add('redux: AssetManager', () => ( - - - - )) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: AssetManager', () => ) .add('no assets', () => ( - - - + )) .add('two assets', () => ( - - - + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts new file mode 100644 index 0000000000000..3b5576667ed26 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AssetType } from '../../../../types'; + +export const AIRPLANE: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'airplane', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const MARKER: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'marker', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const assets = [AIRPLANE, MARKER]; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx index a04d37cf7f9fc..ed000741bc542 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx @@ -17,7 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useNotifyService } from '../../services'; import { ConfirmModal } from '../confirm_modal'; import { Clipboard } from '../clipboard'; @@ -38,11 +38,10 @@ interface Props { } export const Asset: FC = ({ asset, onCreate, onDelete }) => { - const { services } = useKibana(); + const { success } = useNotifyService(); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); - const onCopy = (result: boolean) => - result && services.canvas.notify.success(`Copied '${asset.id}' to clipboard`); + const onCopy = (result: boolean) => result && success(`Copied '${asset.id}' to clipboard`); const confirmModal = ( void; + onSelect: (id: string, embeddableType: string) => void; + availableEmbeddables: string[]; +} + +export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { + const services = useServices(); + const { embeddables, platform } = services; + const { getEmbeddableFactories } = embeddables; + const { getSavedObjects, getUISettings } = platform; + + const onAddPanel = (id: string, savedObjectType: string, name: string) => { + const embeddableFactories = getEmbeddableFactories(); + + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); + + const foundEmbeddableType = found ? found.type : 'unknown'; + + onSelect(id, foundEmbeddableType); + }; + + const embeddableFactories = getEmbeddableFactories(); + + const availableSavedObjects = Array.from(embeddableFactories) + .filter((factory) => { + return availableEmbeddables.includes(factory.type); + }) + .map((factory) => factory.savedObjectMetaData) + .filter>(function ( + maybeSavedObjectMetaData + ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { + return maybeSavedObjectMetaData !== undefined; + }); + + return ( + + + +

{strings.getTitleText()}

+
+
+ + + +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 0b5bd8adf8cb9..8c84e3d7a85d8 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -4,75 +4,107 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; -import { - SavedObjectFinderUi, - SavedObjectMetaData, -} from '../../../../../../src/plugins/saved_objects/public/'; -import { ComponentStrings } from '../../../i18n'; -import { useServices } from '../../services'; - -const { AddEmbeddableFlyout: strings } = ComponentStrings; - -export interface Props { - onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; - availableEmbeddables: string[]; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { compose } from 'recompose'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; +// @ts-expect-error untyped local +import { addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; + +const allowedEmbeddables = { + [EmbeddableTypes.map]: (id: string) => { + return `savedMap id="${id}" | render`; + }, + [EmbeddableTypes.lens]: (id: string) => { + return `savedLens id="${id}" | render`; + }, + [EmbeddableTypes.visualization]: (id: string) => { + return `savedVisualization id="${id}" | render`; + }, + /* + [EmbeddableTypes.search]: (id: string) => { + return `filters | savedSearch id="${id}" | render`; + },*/ +}; + +interface StateProps { + pageId: string; +} + +interface DispatchProps { + addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { - const services = useServices(); - const { embeddables, platform } = services; - const { getEmbeddableFactories } = embeddables; - const { getSavedObjects, getUISettings } = platform; +// FIX: Missing state type +const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => + dispatch(addElement(pageId, partialElement)), +}); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { pageId, ...remainingStateProps } = stateProps; + const { addEmbeddable } = dispatchProps; - const foundEmbeddableType = found ? found.type : 'unknown'; + return { + ...remainingStateProps, + ...ownProps, + onSelect: (id: string, type: string): void => { + const partialElement = { + expression: `markdown "Could not find embeddable for type ${type}" | render`, + }; + if (allowedEmbeddables[type]) { + partialElement.expression = allowedEmbeddables[type](id); + } - onSelect(id, foundEmbeddableType); + addEmbeddable(pageId, partialElement); + ownProps.onClose(); + }, }; - - const embeddableFactories = getEmbeddableFactories(); - - const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) - .map((factory) => factory.savedObjectMetaData) - .filter>(function ( - maybeSavedObjectMetaData - ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { - return maybeSavedObjectMetaData !== undefined; - }); - - return ( - - - -

{strings.getTitleText()}

-
-
- - - -
- ); }; + +export class EmbeddableFlyoutPortal extends React.Component { + el?: HTMLElement; + + constructor(props: ComponentProps) { + super(props); + + this.el = document.createElement('div'); + } + componentDidMount() { + const body = document.querySelector('body'); + if (body && this.el) { + body.appendChild(this.el); + } + } + + componentWillUnmount() { + const body = document.querySelector('body'); + + if (body && this.el) { + body.removeChild(this.el); + } + } + + render() { + if (this.el) { + return ReactDOM.createPortal( + , + this.el + ); + } + } +} + +export const AddEmbeddablePanel = compose void }>( + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts new file mode 100644 index 0000000000000..a7fac10b0c02d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EmbeddableFlyoutPortal, AddEmbeddablePanel } from './flyout'; +export { + AddEmbeddableFlyout as AddEmbeddableFlyoutComponent, + Props as AddEmbeddableFlyoutComponentProps, +} from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx deleted file mode 100644 index 62a073daf4c59..0000000000000 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ /dev/null @@ -1,113 +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 ReactDOM from 'react-dom'; -import { compose } from 'recompose'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { AddEmbeddableFlyout, Props } from './flyout'; -// @ts-expect-error untyped local -import { addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; - -const allowedEmbeddables = { - [EmbeddableTypes.map]: (id: string) => { - return `savedMap id="${id}" | render`; - }, - [EmbeddableTypes.lens]: (id: string) => { - return `savedLens id="${id}" | render`; - }, - [EmbeddableTypes.visualization]: (id: string) => { - return `savedVisualization id="${id}" | render`; - }, - /* - [EmbeddableTypes.search]: (id: string) => { - return `filters | savedSearch id="${id}" | render`; - },*/ -}; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; -} - -// FIX: Missing state type -const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => - dispatch(addElement(pageId, partialElement)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: Props -): Props => { - const { pageId, ...remainingStateProps } = stateProps; - const { addEmbeddable } = dispatchProps; - - return { - ...remainingStateProps, - ...ownProps, - onSelect: (id: string, type: string): void => { - const partialElement = { - expression: `markdown "Could not find embeddable for type ${type}" | render`, - }; - if (allowedEmbeddables[type]) { - partialElement.expression = allowedEmbeddables[type](id); - } - - addEmbeddable(pageId, partialElement); - ownProps.onClose(); - }, - }; -}; - -export class EmbeddableFlyoutPortal extends React.Component { - el?: HTMLElement; - - constructor(props: Props) { - super(props); - - this.el = document.createElement('div'); - } - componentDidMount() { - const body = document.querySelector('body'); - if (body && this.el) { - body.appendChild(this.el); - } - } - - componentWillUnmount() { - const body = document.querySelector('body'); - - if (body && this.el) { - body.removeChild(this.el); - } - } - - render() { - if (this.el) { - return ReactDOM.createPortal( - , - this.el - ); - } - } -} - -export const AddEmbeddablePanel = compose void }>( - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot rename to x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx b/x-pack/plugins/canvas/public/components/expression_input/__stories__/expression_input.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx rename to x-pack/plugins/canvas/public/components/expression_input/__stories__/expression_input.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.stories.storyshot rename to x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.stories.tsx b/x-pack/plugins/canvas/public/components/item_grid/__stories__/item_grid.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.stories.tsx rename to x-pack/plugins/canvas/public/components/item_grid/__stories__/item_grid.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot rename to x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/keyboard_shortcuts_doc.stories.tsx b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/keyboard_shortcuts_doc.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/keyboard_shortcuts_doc.stories.tsx rename to x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/keyboard_shortcuts_doc.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/navbar/navbar.scss b/x-pack/plugins/canvas/public/components/navbar/navbar.scss deleted file mode 100644 index 7b490822763d2..0000000000000 --- a/x-pack/plugins/canvas/public/components/navbar/navbar.scss +++ /dev/null @@ -1,7 +0,0 @@ -.canvasNavbar { - width: 100%; - height: $euiSizeXL * 2; - background-color: darken($euiColorLightestShade, 5%); - position: relative; - z-index: 200; -} diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.ts b/x-pack/plugins/canvas/public/components/page_manager/index.ts index d19540cd6a687..abe7a4a3a5bb1 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/index.ts +++ b/x-pack/plugins/canvas/public/components/page_manager/index.ts @@ -4,28 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { PageManager as Component } from './page_manager'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - pages: getPages(state), - selectedPage: getSelectedPage(state), - workpadId: getWorkpad(state).id, - workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onAddPage: () => dispatch(pageActions.addPage()), - onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), - onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), -}); - -export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PageManager } from './page_manager'; +export { PageManager as PageManagerComponent } from './page_manager.component'; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx rename to x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts new file mode 100644 index 0000000000000..a92f7c6b4c352 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { PageManager as Component } from './page_manager.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + pages: getPages(state), + selectedPage: getSelectedPage(state), + workpadId: getWorkpad(state).id, + workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onAddPage: () => dispatch(pageActions.addPage()), + onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), + onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), +}); + +export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_preview/index.ts b/x-pack/plugins/canvas/public/components/page_preview/index.ts index 25d3254595d2e..22e3861eb9652 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/index.ts +++ b/x-pack/plugins/canvas/public/components/page_preview/index.ts @@ -4,21 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { isWriteable } from '../../state/selectors/workpad'; -import { PagePreview as Component } from './page_preview'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), -}); - -export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PagePreview } from './page_preview'; +export { PagePreview as PagePreviewComponent } from './page_preview.component'; diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx rename to x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts b/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts new file mode 100644 index 0000000000000..8768a2fc169ef --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { isWriteable } from '../../state/selectors/workpad'; +import { PagePreview as Component } from './page_preview.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), +}); + +export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx b/x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_controls.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_controls.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx similarity index 96% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx index 4941d8cb2efa7..a811a296f2e7b 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { SavedElementsModal } from '../saved_elements_modal'; +import { SavedElementsModal } from '../saved_elements_modal.component'; import { testCustomElements } from './fixtures/test_elements'; import { CustomElement } from '../../../../types'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts index da2955c146193..46faf8d14f9b5 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -4,130 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { compose, withState } from 'recompose'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { withServices, WithServicesProps } from '../../services'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../state/actions/transient'; -// @ts-expect-error untyped local -import { insertNodes } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { SavedElementsModal as Component, Props as ComponentProps } from './saved_elements_modal'; -import { State, PositionedElement, CustomElement } from '../../../types'; - -const customElementAdded = 'elements-custom-added'; - -interface OwnProps { - onClose: () => void; -} - -interface OwnPropsWithState extends OwnProps { - customElements: CustomElement[]; - setCustomElements: (customElements: CustomElement[]) => void; - search: string; - setSearch: (search: string) => void; -} - -interface DispatchProps { - selectToplevelNodes: (nodes: PositionedElement[]) => void; - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; -} - -interface StateProps { - pageId: string; -} - -const mapStateToProps = (state: State): StateProps => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes - .filter((e: PositionedElement): boolean => !e.position.parent) - .map((e: PositionedElement): string => e.id) - ) - ), - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: OwnPropsWithState & WithServicesProps -): ComponentProps => { - const { pageId } = stateProps; - const { onClose, search, setCustomElements } = ownProps; - - const findCustomElements = async () => { - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - }; - - return { - ...ownProps, - // add custom element to the page - addCustomElement: (customElement: CustomElement) => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async (text?: string) => { - try { - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't find custom elements`, - }); - } - }, - // remove custom element - removeCustomElement: async (id: string) => { - try { - await customElementService.remove(id); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't delete custom elements`, - }); - } - }, - // update custom element - updateCustomElement: async (id: string, name: string, description: string, image: string) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't update custom elements`, - }); - } - }, - }; -}; - -export const SavedElementsModal = compose( - withServices, - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); +export { SavedElementsModal } from './saved_elements_modal'; +export { + SavedElementsModal as SavedElementsModalComponent, + Props as SavedElementsModalComponentProps, +} from './saved_elements_modal.component'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts new file mode 100644 index 0000000000000..a5c5a2e0adce9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts @@ -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 { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { compose, withState } from 'recompose'; +import { camelCase } from 'lodash'; +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; +import { withServices, WithServicesProps } from '../../services'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-expect-error untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { + SavedElementsModal as Component, + Props as ComponentProps, +} from './saved_elements_modal.component'; +import { State, PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +interface OwnProps { + onClose: () => void; +} + +interface OwnPropsWithState extends OwnProps { + customElements: CustomElement[]; + setCustomElements: (customElements: CustomElement[]) => void; + search: string; + setSearch: (search: string) => void; +} + +interface DispatchProps { + selectToplevelNodes: (nodes: PositionedElement[]) => void; + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; +} + +interface StateProps { + pageId: string; +} + +const mapStateToProps = (state: State): StateProps => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ), + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnPropsWithState & WithServicesProps +): ComponentProps => { + const { pageId } = stateProps; + const { onClose, search, setCustomElements } = ownProps; + + const findCustomElements = async () => { + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + }; + + return { + ...ownProps, + // add custom element to the page + addCustomElement: (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }, + // custom element search + findCustomElements: async (text?: string) => { + try { + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't find custom elements`, + }); + } + }, + // remove custom element + removeCustomElement: async (id: string) => { + try { + await customElementService.remove(id); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }, + // update custom element + updateCustomElement: async (id: string, name: string, description: string, image: string) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't update custom elements`, + }); + } + }, + }; +}; + +export const SavedElementsModal = compose( + withServices, + withState('search', 'setSearch', ''), + withState('customElements', 'setCustomElements', []), + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/shape_picker/__examples__/__snapshots__/shape_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/__snapshots__/shape_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker/__examples__/__snapshots__/shape_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_picker/__stories__/__snapshots__/shape_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_picker/__examples__/shape_picker.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker/__examples__/shape_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/shape_picker_popover.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/shape_picker_popover.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__examples__/__snapshots__/shape_preview.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_preview/__examples__/__snapshots__/shape_preview.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__examples__/shape_preview.stories.tsx b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_preview/__examples__/shape_preview.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/group_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/group_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/multi_element_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/multi_element_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/group_settings.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar/__stories__/group_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/group_settings.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/group_settings.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/multi_element_settings.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar/__stories__/multi_element_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/multi_element_settings.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/multi_element_settings.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx new file mode 100644 index 0000000000000..e3f4e00f4de01 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiTabbedContent } from '@elastic/eui'; +// @ts-expect-error unconverted component +import { Datasource } from '../../datasource'; +// @ts-expect-error unconverted component +import { FunctionFormList } from '../../function_form_list'; +import { PositionedElement } from '../../../../types'; +import { ComponentStrings } from '../../../../i18n'; + +interface Props { + /** + * a Canvas element used to populate config forms + */ + element: PositionedElement; +} + +const { ElementSettings: strings } = ComponentStrings; + +export const ElementSettings: FunctionComponent = ({ element }) => { + const tabs = [ + { + id: 'edit', + name: strings.getDisplayTabLabel(), + content: ( +
+
+ +
+
+ ), + }, + { + id: 'data', + name: strings.getDataTabLabel(), + content: ( +
+ +
+ ), + }, + ]; + + return ; +}; + +ElementSettings.propTypes = { + element: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index e3f4e00f4de01..ba7e31a25daba 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -3,53 +3,32 @@ * 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, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -import { EuiTabbedContent } from '@elastic/eui'; -// @ts-expect-error unconverted component -import { Datasource } from '../../datasource'; -// @ts-expect-error unconverted component -import { FunctionFormList } from '../../function_form_list'; -import { PositionedElement } from '../../../../types'; -import { ComponentStrings } from '../../../../i18n'; +import React from 'react'; +import { connect } from 'react-redux'; +import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; +import { ElementSettings as Component } from './element_settings.component'; +import { State, PositionedElement } from '../../../../types'; interface Props { - /** - * a Canvas element used to populate config forms - */ - element: PositionedElement; + selectedElementId: string; } -const { ElementSettings: strings } = ComponentStrings; +const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ + element: getElementById(state, selectedElementId, getSelectedPage(state)), +}); + +interface StateProps { + element: PositionedElement | undefined; +} -export const ElementSettings: FunctionComponent = ({ element }) => { - const tabs = [ - { - id: 'edit', - name: strings.getDisplayTabLabel(), - content: ( -
-
- -
-
- ), - }, - { - id: 'data', - name: strings.getDataTabLabel(), - content: ( -
- -
- ), - }, - ]; +const renderIfElement: React.FunctionComponent = (props) => { + if (props.element) { + return ; + } - return ; + return null; }; -ElementSettings.propTypes = { - element: PropTypes.object, -}; +export const ElementSettings = connect(mapStateToProps)( + renderIfElement +); diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts new file mode 100644 index 0000000000000..68b90f232fb8b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementSettings } from './element_settings'; +export { ElementSettings as ElementSettingsComponent } from './element_settings.component'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx deleted file mode 100644 index b8d5882234899..0000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx +++ /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 React from 'react'; -import { connect } from 'react-redux'; -import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; -import { ElementSettings as Component } from './element_settings'; -import { State, PositionedElement } from '../../../../types'; - -interface Props { - selectedElementId: string; -} - -const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ - element: getElementById(state, selectedElementId, getSelectedPage(state)), -}); - -interface StateProps { - element: PositionedElement | undefined; -} - -const renderIfElement: React.FunctionComponent = (props) => { - if (props.element) { - return ; - } - - return null; -}; - -export const ElementSettings = connect(mapStateToProps)( - renderIfElement -); diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.stories.storyshot b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.stories.storyshot rename to x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tag/__examples__/tag.stories.tsx b/x-pack/plugins/canvas/public/components/tag/__stories__/tag.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tag/__examples__/tag.stories.tsx rename to x-pack/plugins/canvas/public/components/tag/__stories__/tag.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.stories.storyshot b/x-pack/plugins/canvas/public/components/tag_list/__stories__/__snapshots__/tag_list.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.stories.storyshot rename to x-pack/plugins/canvas/public/components/tag_list/__stories__/__snapshots__/tag_list.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tag_list/__examples__/tag_list.stories.tsx b/x-pack/plugins/canvas/public/components/tag_list/__stories__/tag_list.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tag_list/__examples__/tag_list.stories.tsx rename to x-pack/plugins/canvas/public/components/tag_list/__stories__/tag_list.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/__snapshots__/tool_tip_shortcut.stories.storyshot b/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/__snapshots__/tool_tip_shortcut.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/__snapshots__/tool_tip_shortcut.stories.storyshot rename to x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/__snapshots__/tool_tip_shortcut.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/tool_tip_shortcut.stories.tsx b/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/tool_tip_shortcut.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/tool_tip_shortcut.stories.tsx rename to x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/tool_tip_shortcut.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx deleted file mode 100644 index 5907c932ddabb..0000000000000 --- a/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - TODO: uncomment and fix this test to address storybook errors as a result of nested component dependencies - https://github.com/elastic/kibana/issues/58289 - */ - -/* -import { action } from '@storybook/addon-actions'; -import { storiesOf } from '@storybook/react'; -import React from 'react'; -import { Toolbar } from '../toolbar'; - -storiesOf('components/Toolbar', module) - .addDecorator(story => ( -
- {story()} -
- )) - .add('with null metric', () => ( - - )); -*/ diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot b/x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot new file mode 100644 index 0000000000000..eec0de3c784f1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/Toolbar element selected 1`] = ` +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+`; + +exports[`Storyshots components/Toolbar no element selected 1`] = ` +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx new file mode 100644 index 0000000000000..bd6ad7c8dc499 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { Toolbar } from '../toolbar.component'; + +// @ts-expect-error untyped local +import { getDefaultElement } from '../../../state/defaults'; + +storiesOf('components/Toolbar', module) + .add('no element selected', () => ( + + )) + .add('element selected', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/toolbar/index.js b/x-pack/plugins/canvas/public/components/toolbar/index.js deleted file mode 100644 index 16860063f8a45..0000000000000 --- a/x-pack/plugins/canvas/public/components/toolbar/index.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { pure, compose, withState, getContext, withHandlers } from 'recompose'; -import { canUserWrite } from '../../state/selectors/app'; - -import { - getWorkpad, - getWorkpadName, - getSelectedPageIndex, - getSelectedElement, - isWriteable, -} from '../../state/selectors/workpad'; - -import { Toolbar as Component } from './toolbar'; - -const mapStateToProps = (state) => ({ - workpadName: getWorkpadName(state), - workpadId: getWorkpad(state).id, - totalPages: getWorkpad(state).pages.length, - selectedPageNumber: getSelectedPageIndex(state) + 1, - selectedElement: getSelectedElement(state), - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -export const Toolbar = compose( - pure, - connect(mapStateToProps), - getContext({ - router: PropTypes.object, - }), - withHandlers({ - nextPage: (props) => () => { - const pageNumber = Math.min(props.selectedPageNumber + 1, props.totalPages); - props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); - }, - previousPage: (props) => () => { - const pageNumber = Math.max(1, props.selectedPageNumber - 1); - props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); - }, - }), - withState('tray', 'setTray', (props) => props.tray), - withState('showWorkpadManager', 'setShowWorkpadManager', false) -)(Component); diff --git a/x-pack/plugins/canvas/storybook/addons.js b/x-pack/plugins/canvas/public/components/toolbar/index.ts similarity index 66% rename from x-pack/plugins/canvas/storybook/addons.js rename to x-pack/plugins/canvas/public/components/toolbar/index.ts index 75bbe620c9e7b..dfa730307dafb 100644 --- a/x-pack/plugins/canvas/storybook/addons.js +++ b/x-pack/plugins/canvas/public/components/toolbar/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import '@storybook/addon-actions/register'; -import '@storybook/addon-knobs/register'; -import '@storybook/addon-console'; +export { Toolbar } from './toolbar'; +export { Toolbar as ToolbarComponent } from './toolbar.component'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx similarity index 64% rename from x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx rename to x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index c5475b2559444..6905b3ed23d3f 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC, useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, @@ -16,72 +16,77 @@ import { EuiModalFooter, EuiButton, } from '@elastic/eui'; -import { CanvasElement } from '../../../types'; - -import { ComponentStrings } from '../../../i18n'; -// @ts-expect-error untyped local -import { Navbar } from '../navbar'; // @ts-expect-error untyped local import { WorkpadManager } from '../workpad_manager'; +import { RouterContext } from '../router'; import { PageManager } from '../page_manager'; // @ts-expect-error untyped local import { Expression } from '../expression'; import { Tray } from './tray'; +import { CanvasElement } from '../../../types'; +import { ComponentStrings } from '../../../i18n'; + const { Toolbar: strings } = ComponentStrings; -enum TrayType { - pageManager = 'pageManager', - expression = 'expression', -} +type TrayType = 'pageManager' | 'expression'; interface Props { - workpadName: string; isWriteable: boolean; - canUserWrite: boolean; - tray: TrayType | null; - setTray: (tray: TrayType | null) => void; - - previousPage: () => void; - nextPage: () => void; + selectedElement?: CanvasElement; selectedPageNumber: number; totalPages: number; - - selectedElement: CanvasElement; - - showWorkpadManager: boolean; - setShowWorkpadManager: (show: boolean) => void; + workpadId: string; + workpadName: string; } -export const Toolbar = (props: Props) => { - const { - selectedElement, - tray, - setTray, - previousPage, - nextPage, - selectedPageNumber, - workpadName, - totalPages, - showWorkpadManager, - setShowWorkpadManager, - isWriteable, - } = props; +export const Toolbar: FC = ({ + isWriteable, + selectedElement, + selectedPageNumber, + totalPages, + workpadId, + workpadName, +}) => { + const [activeTray, setActiveTray] = useState(null); + const [showWorkpadManager, setShowWorkpadManager] = useState(false); + const router = useContext(RouterContext); + + // While the tray doesn't get activated if the workpad isn't writeable, + // this effect will ensure that if the tray is open and the workpad + // changes its writeable state, the tray will close. + useEffect(() => { + if (!isWriteable && activeTray === 'expression') { + setActiveTray(null); + } + }, [isWriteable, activeTray]); - const elementIsSelected = Boolean(selectedElement); + if (!router) { + return
{strings.getErrorMessage('Router Undefined')}
; + } - const done = () => setTray(null); + const nextPage = () => { + const page = Math.min(selectedPageNumber + 1, totalPages); + router.navigateTo('loadWorkpad', { id: workpadId, page }); + }; - if (!isWriteable && tray === TrayType.expression) { - done(); - } + const previousPage = () => { + const page = Math.max(1, selectedPageNumber - 1); + router.navigateTo('loadWorkpad', { id: workpadId, page }); + }; - const showHideTray = (exp: TrayType) => { - if (tray && tray === exp) { - return done(); + const elementIsSelected = Boolean(selectedElement); + + const toggleTray = (tray: TrayType) => { + if (activeTray === tray) { + setActiveTray(null); + } else { + if (!isWriteable && tray === 'expression') { + return; + } + setActiveTray(tray); } - setTray(exp); }; const closeWorkpadManager = () => setShowWorkpadManager(false); @@ -102,13 +107,13 @@ export const Toolbar = (props: Props) => { const trays = { pageManager: , - expression: !elementIsSelected ? null : , + expression: !elementIsSelected ? null : setActiveTray(null)} />, }; return (
- {tray !== null && {trays[tray]}} - + {activeTray !== null && setActiveTray(null)}>{trays[activeTray]}} +
openWorkpadManager()}> @@ -126,7 +131,7 @@ export const Toolbar = (props: Props) => { /> - showHideTray(TrayType.pageManager)}> + toggleTray('pageManager')}> {strings.getPageButtonLabel(selectedPageNumber, totalPages)} @@ -145,7 +150,7 @@ export const Toolbar = (props: Props) => { showHideTray(TrayType.expression)} + onClick={() => toggleTray('expression')} data-test-subj="canvasExpressionEditorButton" > {strings.getEditorButtonLabel()} @@ -153,23 +158,17 @@ export const Toolbar = (props: Props) => { )} - - +
{showWorkpadManager && workpadManager}
); }; Toolbar.propTypes = { - workpadName: PropTypes.string, - tray: PropTypes.string, - setTray: PropTypes.func.isRequired, - nextPage: PropTypes.func.isRequired, - previousPage: PropTypes.func.isRequired, + isWriteable: PropTypes.bool.isRequired, + selectedElement: PropTypes.object, selectedPageNumber: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired, - selectedElement: PropTypes.object, - showWorkpadManager: PropTypes.bool.isRequired, - setShowWorkpadManager: PropTypes.func.isRequired, - isWriteable: PropTypes.bool.isRequired, + workpadId: PropTypes.string.isRequired, + workpadName: PropTypes.string.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss index 7303f43dd269f..41bc718dcfec1 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss @@ -24,3 +24,11 @@ padding: $euiSizeM; height: 100%; } + +.canvasToolbar__container { + width: 100%; + height: $euiSizeXL * 2; + background-color: darken($euiColorLightestShade, 5%); + position: relative; + z-index: 200; +} diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts b/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts new file mode 100644 index 0000000000000..f93b42cb442b8 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { canUserWrite } from '../../state/selectors/app'; + +import { + getWorkpad, + getWorkpadName, + getSelectedPageIndex, + getSelectedElement, + isWriteable, +} from '../../state/selectors/workpad'; + +import { Toolbar as ToolbarComponent } from './toolbar.component'; +import { State } from '../../../types'; + +export const Toolbar = connect((state: State) => ({ + workpadName: getWorkpadName(state), + workpadId: getWorkpad(state).id, + totalPages: getWorkpad(state).pages.length, + selectedPageNumber: getSelectedPageIndex(state) + 1, + selectedElement: getSelectedElement(state), + isWriteable: isWriteable(state) && canUserWrite(state), +}))(ToolbarComponent); diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts b/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts index 1343bc8d01e9a..18c45190cbd48 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Tray as Component } from './tray'; - -export const Tray = pure(Component); +export { Tray } from './tray'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx index 2c0b4e69c240b..0699d30833ecd 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode, Fragment, MouseEventHandler } from 'react'; +import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; @@ -18,7 +18,7 @@ interface Props { export const Tray = ({ children, done }: Props) => { return ( - + <> { /> -
{children}
-
+ ); }; Tray.propTypes = { - children: PropTypes.node, - done: PropTypes.func, + children: PropTypes.node.isRequired, + done: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/delete_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/delete_var.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/edit_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/edit_var.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/var_config.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/delete_var.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/delete_var.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/delete_var.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/delete_var.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/edit_var.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/edit_var.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/edit_var.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/edit_var.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/var_config.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/var_config.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/var_config.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/var_config.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts index abd40731078ec..34e3d3ff4b057 100644 --- a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts @@ -4,20 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { addColor, removeColor } from '../../state/actions/workpad'; -import { getWorkpadColors } from '../../state/selectors/workpad'; - -import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - colors: getWorkpadColors(state), -}); - -const mapDispatchToProps = { - onAddColor: addColor, - onRemoveColor: removeColor, -}; - -export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadColorPicker } from './workpad_color_picker'; +export { WorkpadColorPicker as WorkpadColorPickerComponent } from './workpad_color_picker.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx rename to x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts new file mode 100644 index 0000000000000..2f4b0fe7b4ec1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { addColor, removeColor } from '../../state/actions/workpad'; +import { getWorkpadColors } from '../../state/selectors/workpad'; + +import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + colors: getWorkpadColors(state), +}); + +const mapDispatchToProps = { + onAddColor: addColor, + onRemoveColor: removeColor, +}; + +export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_config/index.ts b/x-pack/plugins/canvas/public/components/workpad_config/index.ts index bba08d7647e9e..63db96ca5aef9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_config/index.ts @@ -4,40 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; - -import { get } from 'lodash'; -import { - sizeWorkpad as setSize, - setName, - setWorkpadCSS, - updateWorkpadVariables, -} from '../../state/actions/workpad'; - -import { getWorkpad } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { WorkpadConfig as Component } from './workpad_config'; -import { State, CanvasVariable } from '../../../types'; - -const mapStateToProps = (state: State) => { - const workpad = getWorkpad(state); - - return { - name: get(workpad, 'name'), - size: { - width: get(workpad, 'width'), - height: get(workpad, 'height'), - }, - css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), - variables: get(workpad, 'variables', []), - }; -}; - -const mapDispatchToProps = { - setSize, - setName, - setWorkpadCSS, - setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), -}; - -export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadConfig } from './workpad_config'; +export { WorkpadConfig as WorkpadConfigComponent } from './workpad_config.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx rename to x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts new file mode 100644 index 0000000000000..e4ddf31141972 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; + +import { get } from 'lodash'; +import { + sizeWorkpad as setSize, + setName, + setWorkpadCSS, + updateWorkpadVariables, +} from '../../state/actions/workpad'; + +import { getWorkpad } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { WorkpadConfig as Component } from './workpad_config.component'; +import { State, CanvasVariable } from '../../../types'; + +const mapStateToProps = (state: State) => { + const workpad = getWorkpad(state); + + return { + name: get(workpad, 'name'), + size: { + width: get(workpad, 'width'), + height: get(workpad, 'height'), + }, + css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), + variables: get(workpad, 'variables', []), + }; +}; + +const mapDispatchToProps = { + setSize, + setName, + setWorkpadCSS, + setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), +}; + +export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx similarity index 98% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx index 8bbc3e09af4bf..be6247b0bbcab 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { EditMenu } from '../edit_menu'; +import { EditMenu } from '../edit_menu.component'; import { PositionedElement } from '../../../../../types'; const handlers = { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts new file mode 100644 index 0000000000000..3a2264c05eb4b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, PositionedElement } from '../../../../types'; +import { getClipboardData } from '../../../lib/clipboard'; +// @ts-expect-error untyped local +import { flatten } from '../../../lib/aeroelastic/functional'; +// @ts-expect-error untyped local +import { globalStateUpdater } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { crawlTree } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { undoHistory, redoHistory } from '../../../state/actions/history'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { + getSelectedPage, + getNodes, + getSelectedToplevelNodes, +} from '../../../state/selectors/workpad'; +import { + layerHandlerCreators, + clipboardHandlerCreators, + basicHandlerCreators, + groupHandlerCreators, + alignmentDistributionHandlerCreators, +} from '../../../lib/element_handler_creators'; +import { EditMenu as Component, Props as ComponentProps } from './edit_menu.component'; + +type LayoutState = any; + +type CommitFn = (type: string, payload: any) => LayoutState; + +interface OwnProps { + commit: CommitFn; +} + +const withGlobalState = ( + commit: CommitFn, + updateGlobalState: (layoutState: LayoutState) => void +) => (type: string, payload: any) => { + const newLayoutState = commit(type, payload); + if (newLayoutState.currentScene.gestureEnd) { + updateGlobalState(newLayoutState); + } +}; + +/* + * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts + */ +const mapStateToProps = (state: State) => { + const pageId = getSelectedPage(state); + const nodes = getNodes(state, pageId) as PositionedElement[]; + const selectedToplevelNodes = getSelectedToplevelNodes(state); + + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) + .filter((shape?: PositionedElement) => shape) as PositionedElement[]; + + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map((shape: PositionedElement) => + nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? + ? [shape.id] + : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) + ) + ); + + const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + const selectedNodes = selectedNodeIds + .map((id: string) => nodes.find((s) => s.id === id)) + .filter((node: PositionedElement | undefined): node is PositionedElement => { + return !!node; + }); + + return { + pageId, + selectedToplevelNodes, + selectedNodes, + state, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) + ) + ), + elementLayer: (pageId: string, elementId: string, movement: number) => { + dispatch(elementLayer({ pageId, elementId, movement })); + }, + undoHistory: () => dispatch(undoHistory()), + redoHistory: () => dispatch(redoHistory()), + dispatch, +}); + +const mergeProps = ( + { state, selectedToplevelNodes, ...restStateProps }: ReturnType, + { dispatch, ...restDispatchProps }: ReturnType, + { commit }: OwnProps +) => { + const updateGlobalState = globalStateUpdater(dispatch, state); + + return { + ...restDispatchProps, + ...restStateProps, + commit: withGlobalState(commit, updateGlobalState), + groupIsSelected: + selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), + }; +}; + +export const EditMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators), + withHandlers(layerHandlerCreators), + withHandlers(groupHandlerCreators), + withHandlers(alignmentDistributionHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts index 8f013f70aefcd..0db425f01cccd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts @@ -4,130 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, PositionedElement } from '../../../../types'; -import { getClipboardData } from '../../../lib/clipboard'; -// @ts-expect-error untyped local -import { flatten } from '../../../lib/aeroelastic/functional'; -// @ts-expect-error untyped local -import { globalStateUpdater } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { crawlTree } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { undoHistory, redoHistory } from '../../../state/actions/history'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../../state/actions/transient'; -import { - getSelectedPage, - getNodes, - getSelectedToplevelNodes, -} from '../../../state/selectors/workpad'; -import { - layerHandlerCreators, - clipboardHandlerCreators, - basicHandlerCreators, - groupHandlerCreators, - alignmentDistributionHandlerCreators, -} from '../../../lib/element_handler_creators'; -import { EditMenu as Component, Props as ComponentProps } from './edit_menu'; - -type LayoutState = any; - -type CommitFn = (type: string, payload: any) => LayoutState; - -interface OwnProps { - commit: CommitFn; -} - -const withGlobalState = ( - commit: CommitFn, - updateGlobalState: (layoutState: LayoutState) => void -) => (type: string, payload: any) => { - const newLayoutState = commit(type, payload); - if (newLayoutState.currentScene.gestureEnd) { - updateGlobalState(newLayoutState); - } -}; - -/* - * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts - */ -const mapStateToProps = (state: State) => { - const pageId = getSelectedPage(state); - const nodes = getNodes(state, pageId) as PositionedElement[]; - const selectedToplevelNodes = getSelectedToplevelNodes(state); - - const selectedPrimaryShapeObjects = selectedToplevelNodes - .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) - .filter((shape?: PositionedElement) => shape) as PositionedElement[]; - - const selectedPersistentPrimaryNodes = flatten( - selectedPrimaryShapeObjects.map((shape: PositionedElement) => - nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? - ? [shape.id] - : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) - ) - ); - - const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); - const selectedNodes = selectedNodeIds - .map((id: string) => nodes.find((s) => s.id === id)) - .filter((node: PositionedElement | undefined): node is PositionedElement => { - return !!node; - }); - - return { - pageId, - selectedToplevelNodes, - selectedNodes, - state, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), - removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) - ) - ), - elementLayer: (pageId: string, elementId: string, movement: number) => { - dispatch(elementLayer({ pageId, elementId, movement })); - }, - undoHistory: () => dispatch(undoHistory()), - redoHistory: () => dispatch(redoHistory()), - dispatch, -}); - -const mergeProps = ( - { state, selectedToplevelNodes, ...restStateProps }: ReturnType, - { dispatch, ...restDispatchProps }: ReturnType, - { commit }: OwnProps -) => { - const updateGlobalState = globalStateUpdater(dispatch, state); - - return { - ...restDispatchProps, - ...restStateProps, - commit: withGlobalState(commit, updateGlobalState), - groupIsSelected: - selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), - }; -}; - -export const EditMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), - withHandlers(basicHandlerCreators), - withHandlers(clipboardHandlerCreators), - withHandlers(layerHandlerCreators), - withHandlers(groupHandlerCreators), - withHandlers(alignmentDistributionHandlerCreators) -)(Component); +export { EditMenu } from './edit_menu'; +export { EditMenu as EditMenuComponent } from './edit_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx similarity index 98% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx index 9aca5ce33ba02..cf9b334ffe8ea 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ElementSpec } from '../../../../../types'; -import { ElementMenu } from '../element_menu'; +import { ElementMenu } from '../element_menu.component'; const testElements: { [key: string]: ElementSpec } = { areaChart: { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx new file mode 100644 index 0000000000000..6d9233aaba22b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy } from 'lodash'; +import React, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ElementSpec } from '../../../../types'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { getId } from '../../../lib/get_id'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { AssetManager } from '../../asset_manager'; +import { SavedElementsModal } from '../../saved_elements_modal'; + +interface CategorizedElementLists { + [key: string]: ElementSpec[]; +} + +interface ElementTypeMeta { + [key: string]: { name: string; icon: string }; +} + +export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; + +// label and icon for the context menu item for each element type +const elementTypeMeta: ElementTypeMeta = { + chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, + filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, + image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, + other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, + progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, + shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, + text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, +}; + +const getElementType = (element: ElementSpec): string => + element && element.type && Object.keys(elementTypeMeta).includes(element.type) + ? element.type + : 'other'; + +const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { + elements = sortBy(elements, 'displayName'); + + const categories: CategorizedElementLists = { other: [] }; + + elements.forEach((element: ElementSpec) => { + const type = getElementType(element); + + if (categories[type]) { + categories[type].push(element); + } else { + categories[type] = [element]; + } + }); + + return categories; +}; + +export interface Props { + /** + * Dictionary of elements from elements registry + */ + elements: { [key: string]: ElementSpec }; + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: ElementSpec) => void; + /** + * Renders embeddable flyout + */ + renderEmbedPanel: (onClose: () => void) => JSX.Element; +} + +export const ElementMenu: FunctionComponent = ({ + elements, + addElement, + renderEmbedPanel, +}) => { + const [isAssetModalVisible, setAssetModalVisible] = useState(false); + const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); + const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); + + const hideAssetModal = () => setAssetModalVisible(false); + const showAssetModal = () => setAssetModalVisible(true); + const hideEmbedPanel = () => setEmbedPanelVisible(false); + const showEmbedPanel = () => setEmbedPanelVisible(true); + const hideSavedElementsModal = () => setSavedElementsModalVisible(false); + const showSavedElementsModal = () => setSavedElementsModalVisible(true); + + const { + chart: chartElements, + filter: filterElements, + image: imageElements, + other: otherElements, + progress: progressElements, + shape: shapeElements, + text: textElements, + } = categorizeElementsByType(Object.values(elements)); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ + name: element.displayName || element.name, + icon: element.icon, + onClick: () => { + addElement(element); + closePopover(); + }, + }); + + const elementListToMenuItems = (elementList: ElementSpec[]) => { + const type = getElementType(elementList[0]); + const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; + + if (elementList.length > 1) { + return { + name, + icon: , + panel: { + id: getId('element-type'), + title: name, + items: elementList.map(elementToMenuItem), + }, + }; + } + + return elementToMenuItem(elementList[0]); + }; + + return { + id: 0, + items: [ + elementListToMenuItems(textElements), + elementListToMenuItems(shapeElements), + elementListToMenuItems(chartElements), + elementListToMenuItems(imageElements), + elementListToMenuItems(filterElements), + elementListToMenuItems(progressElements), + elementListToMenuItems(otherElements), + { + name: strings.getMyElementsMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'saved-elements-menu-option', + icon: , + onClick: () => { + showSavedElementsModal(); + closePopover(); + }, + }, + { + name: strings.getAssetsMenuItemLabel(), + icon: , + onClick: () => { + showAssetModal(); + closePopover(); + }, + }, + { + name: strings.getEmbedObjectMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + onClick: () => { + showEmbedPanel(); + closePopover(); + }, + }, + ], + }; + }; + + const exportControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getElementMenuButtonLabel()} + + ); + + return ( + + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {isAssetModalVisible ? : null} + {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} + {isSavedElementsModalVisible ? : null} + + ); +}; + +ElementMenu.propTypes = { + elements: PropTypes.object, + addElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index 6d9233aaba22b..2cbe4ae5a6575 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -4,211 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; -import React, { Fragment, FunctionComponent, useState } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiButton, - EuiContextMenu, - EuiIcon, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui'; -import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; -import { ComponentStrings } from '../../../../i18n/components'; -import { ElementSpec } from '../../../../types'; -import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; -import { getId } from '../../../lib/get_id'; -import { Popover, ClosePopoverFn } from '../../popover'; -import { AssetManager } from '../../asset_manager'; -import { SavedElementsModal } from '../../saved_elements_modal'; - -interface CategorizedElementLists { - [key: string]: ElementSpec[]; -} - -interface ElementTypeMeta { - [key: string]: { name: string; icon: string }; +import React from 'react'; +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, ElementSpec } from '../../../../types'; +// @ts-expect-error untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { ElementMenu as Component, Props as ComponentProps } from './element_menu.component'; +// @ts-expect-error untyped local +import { addElement } from '../../../state/actions/elements'; +import { getSelectedPage } from '../../../state/selectors/workpad'; +import { AddEmbeddablePanel } from '../../embeddable_flyout'; + +interface StateProps { + pageId: string; } -export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; - -// label and icon for the context menu item for each element type -const elementTypeMeta: ElementTypeMeta = { - chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, - filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, - image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, - other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, - progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, - shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, - text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, -}; - -const getElementType = (element: ElementSpec): string => - element && element.type && Object.keys(elementTypeMeta).includes(element.type) - ? element.type - : 'other'; - -const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { - elements = sortBy(elements, 'displayName'); - - const categories: CategorizedElementLists = { other: [] }; - - elements.forEach((element: ElementSpec) => { - const type = getElementType(element); - - if (categories[type]) { - categories[type].push(element); - } else { - categories[type] = [element]; - } - }); - - return categories; -}; - -export interface Props { - /** - * Dictionary of elements from elements registry - */ - elements: { [key: string]: ElementSpec }; - /** - * Handler for adding a selected element to the workpad - */ - addElement: (element: ElementSpec) => void; - /** - * Renders embeddable flyout - */ - renderEmbedPanel: (onClose: () => void) => JSX.Element; +interface DispatchProps { + addElement: (pageId: string) => (partialElement: ElementSpec) => void; } -export const ElementMenu: FunctionComponent = ({ - elements, - addElement, - renderEmbedPanel, -}) => { - const [isAssetModalVisible, setAssetModalVisible] = useState(false); - const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); - const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); - - const hideAssetModal = () => setAssetModalVisible(false); - const showAssetModal = () => setAssetModalVisible(true); - const hideEmbedPanel = () => setEmbedPanelVisible(false); - const showEmbedPanel = () => setEmbedPanelVisible(true); - const hideSavedElementsModal = () => setSavedElementsModalVisible(false); - const showSavedElementsModal = () => setSavedElementsModalVisible(true); - - const { - chart: chartElements, - filter: filterElements, - image: imageElements, - other: otherElements, - progress: progressElements, - shape: shapeElements, - text: textElements, - } = categorizeElementsByType(Object.values(elements)); - - const getPanelTree = (closePopover: ClosePopoverFn) => { - const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ - name: element.displayName || element.name, - icon: element.icon, - onClick: () => { - addElement(element); - closePopover(); - }, - }); - - const elementListToMenuItems = (elementList: ElementSpec[]) => { - const type = getElementType(elementList[0]); - const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; - - if (elementList.length > 1) { - return { - name, - icon: , - panel: { - id: getId('element-type'), - title: name, - items: elementList.map(elementToMenuItem), - }, - }; - } - - return elementToMenuItem(elementList[0]); - }; - - return { - id: 0, - items: [ - elementListToMenuItems(textElements), - elementListToMenuItems(shapeElements), - elementListToMenuItems(chartElements), - elementListToMenuItems(imageElements), - elementListToMenuItems(filterElements), - elementListToMenuItems(progressElements), - elementListToMenuItems(otherElements), - { - name: strings.getMyElementsMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - 'data-test-subj': 'saved-elements-menu-option', - icon: , - onClick: () => { - showSavedElementsModal(); - closePopover(); - }, - }, - { - name: strings.getAssetsMenuItemLabel(), - icon: , - onClick: () => { - showAssetModal(); - closePopover(); - }, - }, - { - name: strings.getEmbedObjectMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - icon: , - onClick: () => { - showEmbedPanel(); - closePopover(); - }, - }, - ], - }; - }; - - const exportControl = (togglePopover: React.MouseEventHandler) => ( - - {strings.getElementMenuButtonLabel()} - - ); - - return ( - - - {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - - )} - - {isAssetModalVisible ? : null} - {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} - {isSavedElementsModalVisible ? : null} - - ); -}; - -ElementMenu.propTypes = { - elements: PropTypes.object, - addElement: PropTypes.func.isRequired, -}; +const mapStateToProps = (state: State) => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), +}); + +const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ + ...stateProps, + ...dispatchProps, + addElement: dispatchProps.addElement(stateProps.pageId), + // Moved this section out of the main component to enable stories + renderEmbedPanel: (onClose: () => void) => , +}); + +export const ElementMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ elements: elementsRegistry.toJS() })) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts new file mode 100644 index 0000000000000..26f81e125f6e2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementMenu } from './element_menu'; +export { ElementMenu as ElementMenuComponent } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx deleted file mode 100644 index 264873fc994dd..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.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 from 'react'; -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, ElementSpec } from '../../../../types'; -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; -// @ts-expect-error untyped local -import { addElement } from '../../../state/actions/elements'; -import { getSelectedPage } from '../../../state/selectors/workpad'; -import { AddEmbeddablePanel } from '../../embeddable_flyout'; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addElement: (pageId: string) => (partialElement: ElementSpec) => void; -} - -const mapStateToProps = (state: State) => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), -}); - -const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ - ...stateProps, - ...dispatchProps, - addElement: dispatchProps.addElement(stateProps.pageId), - // Moved this section out of the main component to enable stories - renderEmbedPanel: (onClose: () => void) => , -}); - -export const ElementMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ elements: elementsRegistry.toJS() })) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/index.ts new file mode 100644 index 0000000000000..0b6f8cc06d198 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WorkpadHeader } from './workpad_header'; +export { WorkpadHeader as WorkpadHeaderComponent } from './workpad_header.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx deleted file mode 100644 index 407b4ff932811..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx +++ /dev/null @@ -1,46 +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 { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; -import { setWriteable } from '../../state/actions/workpad'; -import { State } from '../../../types'; -import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header'; - -interface StateProps { - isWriteable: boolean; - canUserWrite: boolean; - selectedPage: string; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; -} - -const mapStateToProps = (state: State): StateProps => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - canUserWrite: canUserWrite(state), - selectedPage: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), -}); - -export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts index 87b926d93ccb9..8db62f5ac2d87 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts @@ -4,19 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -import { getInFlight } from '../../../state/selectors/resolved_args'; -import { State } from '../../../../types'; -import { RefreshControl as Component } from './refresh_control'; - -const mapStateToProps = (state: State) => ({ - inFlight: getInFlight(state), -}); - -const mapDispatchToProps = { - doRefresh: fetchAllRenderables, -}; - -export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); +export { RefreshControl } from './refresh_control'; +export { RefreshControl as RefreshControlComponent } from './refresh_control.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts new file mode 100644 index 0000000000000..a7f01e46927ce --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +import { getInFlight } from '../../../state/selectors/resolved_args'; +import { State } from '../../../../types'; +import { RefreshControl as Component } from './refresh_control.component'; + +const mapStateToProps = (state: State) => ({ + inFlight: getInFlight(state), +}); + +const mapDispatchToProps = { + doRefresh: fetchAllRenderables, +}; + +export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/pdf_panel.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/pdf_panel.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx similarity index 92% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx index ab9137b1676c9..e0a1f0e381fd3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ShareMenu } from '../share_menu'; +import { ShareMenu } from '../share_menu.component'; storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( { + const renderers: string[] = []; + const expressions = getRenderedWorkpadExpressions(state); + expressions.forEach((expression) => { + if (!renderFunctionNames.includes(expression)) { + renderers.push(expression); + } + }); + + return renderers; +}; + +const mapStateToProps = (state: State) => ({ + renderedWorkpad: getRenderedWorkpad(state), + unsupportedRenderers: getUnsupportedRenderers(state), + workpad: getWorkpad(state), +}); + +interface Props { + onClose: OnCloseFn; + renderedWorkpad: CanvasRenderedWorkpad; + unsupportedRenderers: string[]; + workpad: CanvasWorkpad; +} + +export const ShareWebsiteFlyout = compose>( + connect(mapStateToProps), + withKibana, + withProps( + ({ + unsupportedRenderers, + renderedWorkpad, + onClose, + workpad, + kibana, + }: Props & WithKibanaProps): ComponentProps => ({ + unsupportedRenderers, + onClose, + onCopy: () => { + kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); + }, + onDownload: (type) => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(kibana.services.http.basePath.get()); + return; + case 'shareZip': + const basePath = kibana.services.http.basePath.get(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then((blob) => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getShareableZipErrorTitle(workpad.name), + }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts index 1e1eac2a1dcf3..335c5dff6ed74 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { - getWorkpad, - getRenderedWorkpad, - getRenderedWorkpadExpressions, -} from '../../../../state/selectors/workpad'; -import { - downloadRenderedWorkpad, - downloadRuntime, - downloadZippedRuntime, -} from '../../../../lib/download_workpad'; -import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; -import { State, CanvasWorkpad } from '../../../../../types'; -import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; -import { arrayBufferFetch } from '../../../../../common/lib/fetch'; -import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; -import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; - -import { ComponentStrings } from '../../../../../i18n/components'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; -import { OnCloseFn } from '../share_menu'; -import { WithKibanaProps } from '../../../../index'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const getUnsupportedRenderers = (state: State) => { - const renderers: string[] = []; - const expressions = getRenderedWorkpadExpressions(state); - expressions.forEach((expression) => { - if (!renderFunctionNames.includes(expression)) { - renderers.push(expression); - } - }); - - return renderers; -}; - -const mapStateToProps = (state: State) => ({ - renderedWorkpad: getRenderedWorkpad(state), - unsupportedRenderers: getUnsupportedRenderers(state), - workpad: getWorkpad(state), -}); - -interface Props { - onClose: OnCloseFn; - renderedWorkpad: CanvasRenderedWorkpad; - unsupportedRenderers: string[]; - workpad: CanvasWorkpad; -} - -export const ShareWebsiteFlyout = compose>( - connect(mapStateToProps), - withKibana, - withProps( - ({ - unsupportedRenderers, - renderedWorkpad, - onClose, - workpad, - kibana, - }: Props & WithKibanaProps): ComponentProps => ({ - unsupportedRenderers, - onClose, - onCopy: () => { - kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); - }, - onDownload: (type) => { - switch (type) { - case 'share': - downloadRenderedWorkpad(renderedWorkpad); - return; - case 'shareRuntime': - downloadRuntime(kibana.services.http.basePath.get()); - return; - case 'shareZip': - const basePath = kibana.services.http.basePath.get(); - arrayBufferFetch - .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) - .then((blob) => downloadZippedRuntime(blob.data)) - .catch((err: Error) => { - kibana.services.canvas.notify.error(err, { - title: strings.getShareableZipErrorTitle(workpad.name), - }); - }); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareWebsiteFlyout } from './flyout'; +export { ShareWebsiteFlyout as ShareWebsiteFlyoutComponent } from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx index ea8aba688b2a6..b38226bb12a23 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteRuntimeStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index 81f559651eb25..42497fcd316fe 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -19,7 +19,7 @@ import { import { ComponentStrings } from '../../../../../i18n/components'; import { Clipboard } from '../../../clipboard'; -import { OnCopyFn } from './share_website_flyout'; +import { OnCopyFn } from './flyout'; const { ShareWebsiteSnippetsStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx index 1a5884d89d066..ac4dfe6872d3c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteWorkpadStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts index 01bcfebc0dba9..19dc9b668e61a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; -import { getWorkpad, getPages } from '../../../state/selectors/workpad'; -import { getWindow } from '../../../lib/get_window'; -import { downloadWorkpad } from '../../../lib/download_workpad'; -import { ShareMenu as Component, Props as ComponentProps } from './share_menu'; -import { getPdfUrl, createPdf } from './utils'; -import { State, CanvasWorkpad } from '../../../../types'; -import { withServices, WithServicesProps } from '../../../services'; - -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const mapStateToProps = (state: State) => ({ - workpad: getWorkpad(state), - pageCount: getPages(state).length, -}); - -const getAbsoluteUrl = (path: string) => { - const { location } = getWindow(); - - if (!location) { - return path; - } // fallback for mocked window object - - const { protocol, hostname, port } = location; - return `${protocol}//${hostname}:${port}${path}`; -}; - -interface Props { - workpad: CanvasWorkpad; - pageCount: number; -} - -export const ShareMenu = compose( - connect(mapStateToProps), - withServices, - withProps( - ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ - getExportUrl: (type) => { - if (type === 'pdf') { - const pdfUrl = getPdfUrl( - workpad, - { pageCount }, - services.platform.getBasePathInterface() - ); - return getAbsoluteUrl(pdfUrl); - } - - throw new Error(strings.getUnknownExportErrorMessage(type)); - }, - onCopy: (type) => { - switch (type) { - case 'pdf': - services.notify.info(strings.getCopyPDFMessage()); - break; - case 'reportingConfig': - services.notify.info(strings.getCopyReportingConfigMessage()); - break; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - onExport: (type) => { - switch (type) { - case 'pdf': - return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) - .then(({ data }: { data: { job: { id: string } } }) => { - services.notify.info(strings.getExportPDFMessage(), { - title: strings.getExportPDFTitle(workpad.name), - }); - - // register the job so a completion notification shows up when it's ready - jobCompletionNotifications.add(data.job.id); - }) - .catch((err: Error) => { - services.notify.error(err, { - title: strings.getExportPDFErrorTitle(workpad.name), - }); - }); - case 'json': - downloadWorkpad(workpad.id); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareMenu } from './share_menu'; +export { ShareMenu as ShareMenuComponent } from './share_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts new file mode 100644 index 0000000000000..85c4b14a28c13 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; +import { getWorkpad, getPages } from '../../../state/selectors/workpad'; +import { getWindow } from '../../../lib/get_window'; +import { downloadWorkpad } from '../../../lib/download_workpad'; +import { ShareMenu as Component, Props as ComponentProps } from './share_menu.component'; +import { getPdfUrl, createPdf } from './utils'; +import { State, CanvasWorkpad } from '../../../../types'; +import { withServices, WithServicesProps } from '../../../services'; + +import { ComponentStrings } from '../../../../i18n'; + +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +const mapStateToProps = (state: State) => ({ + workpad: getWorkpad(state), + pageCount: getPages(state).length, +}); + +const getAbsoluteUrl = (path: string) => { + const { location } = getWindow(); + + if (!location) { + return path; + } // fallback for mocked window object + + const { protocol, hostname, port } = location; + return `${protocol}//${hostname}:${port}${path}`; +}; + +interface Props { + workpad: CanvasWorkpad; + pageCount: number; +} + +export const ShareMenu = compose( + connect(mapStateToProps), + withServices, + withProps( + ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ + getExportUrl: (type) => { + if (type === 'pdf') { + const pdfUrl = getPdfUrl( + workpad, + { pageCount }, + services.platform.getBasePathInterface() + ); + return getAbsoluteUrl(pdfUrl); + } + + throw new Error(strings.getUnknownExportErrorMessage(type)); + }, + onCopy: (type) => { + switch (type) { + case 'pdf': + services.notify.info(strings.getCopyPDFMessage()); + break; + case 'reportingConfig': + services.notify.info(strings.getCopyReportingConfigMessage()); + break; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + onExport: (type) => { + switch (type) { + case 'pdf': + return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) + .then(({ data }: { data: { job: { id: string } } }) => { + services.notify.info(strings.getExportPDFMessage(), { + title: strings.getExportPDFTitle(workpad.name), + }); + + // register the job so a completion notification shows up when it's ready + jobCompletionNotifications.add(data.job.id); + }) + .catch((err: Error) => { + services.notify.error(err, { + title: strings.getExportPDFErrorTitle(workpad.name), + }); + }); + case 'json': + downloadWorkpad(workpad.id); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/__snapshots__/view_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/__snapshots__/view_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx similarity index 97% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx index 5b4de05da3a3d..6b033feb26021 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ViewMenu } from '../view_menu'; +import { ViewMenu } from '../view_menu.component'; const handlers = { setZoomScale: action('setZoomScale'), diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts index e2a05d13b017e..167b3822fd13d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -4,97 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers } from 'recompose'; -import { Dispatch } from 'redux'; -import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; -import { State, CanvasWorkpadBoundingBox } from '../../../../types'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; -import { - setWriteable, - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, -} from '../../../state/actions/workpad'; -import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; -import { - getWorkpadBoundingBox, - getWorkpadWidth, - getWorkpadHeight, - isWriteable, - getRefreshInterval, - getAutoplay, -} from '../../../state/selectors/workpad'; -import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; -import { getFitZoomScale } from './lib/get_fit_zoom_scale'; - -interface StateProps { - zoomScale: number; - boundingBox: CanvasWorkpadBoundingBox; - workpadWidth: number; - workpadHeight: number; - isWriteable: boolean; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; - setZoomScale: (scale: number) => void; - setFullscreen: (showFullscreen: boolean) => void; -} - -const mapStateToProps = (state: State) => { - const { enabled, interval } = getAutoplay(state); - - return { - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - isWriteable: isWriteable(state) && canUserWrite(state), - refreshInterval: getRefreshInterval(state), - autoplayEnabled: enabled, - autoplayInterval: interval, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), - setFullscreen: (value: boolean) => { - dispatch(setFullscreen(value)); - - if (value) { - dispatch(selectToplevelNodes([])); - } - }, - doRefresh: () => dispatch(fetchAllRenderables()), - setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), - enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), - setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => { - const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; - - return { - ...remainingStateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), - enterFullscreen: () => dispatchProps.setFullscreen(true), - fitToWindow: () => - dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), - }; -}; - -export const ViewMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withHandlers(zoomHandlerCreators) -)(Component); +export { ViewMenu } from './view_menu'; +export { ViewMenu as ViewMenuComponent } from './view_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts new file mode 100644 index 0000000000000..c9650a35ea2a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { Dispatch } from 'redux'; +import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; +import { State, CanvasWorkpadBoundingBox } from '../../../../types'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +import { + setWriteable, + setRefreshInterval, + enableAutoplay, + setAutoplayInterval, +} from '../../../state/actions/workpad'; +import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; +import { + getWorkpadBoundingBox, + getWorkpadWidth, + getWorkpadHeight, + isWriteable, + getRefreshInterval, + getAutoplay, +} from '../../../state/selectors/workpad'; +import { ViewMenu as Component, Props as ComponentProps } from './view_menu.component'; +import { getFitZoomScale } from './lib/get_fit_zoom_scale'; + +interface StateProps { + zoomScale: number; + boundingBox: CanvasWorkpadBoundingBox; + workpadWidth: number; + workpadHeight: number; + isWriteable: boolean; +} + +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; + setZoomScale: (scale: number) => void; + setFullscreen: (showFullscreen: boolean) => void; +} + +const mapStateToProps = (state: State) => { + const { enabled, interval } = getAutoplay(state); + + return { + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), + refreshInterval: getRefreshInterval(state), + autoplayEnabled: enabled, + autoplayInterval: interval, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), + setFullscreen: (value: boolean) => { + dispatch(setFullscreen(value)); + + if (value) { + dispatch(selectToplevelNodes([])); + } + }, + doRefresh: () => dispatch(fetchAllRenderables()), + setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), + enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), + setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + + return { + ...remainingStateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + enterFullscreen: () => dispatchProps.setFullscreen(true), + fitToWindow: () => + dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), + }; +}; + +export const ViewMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withHandlers(zoomHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx new file mode 100644 index 0000000000000..eb4b451896b46 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +// @ts-expect-error no @types definition +import { Shortcuts } from 'react-shortcuts'; +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n'; +import { ToolTipShortcut } from '../tool_tip_shortcut/'; +import { RefreshControl } from './refresh_control'; +// @ts-expect-error untyped local +import { FullscreenControl } from './fullscreen_control'; +import { EditMenu } from './edit_menu'; +import { ElementMenu } from './element_menu'; +import { ShareMenu } from './share_menu'; +import { ViewMenu } from './view_menu'; + +const { WorkpadHeader: strings } = ComponentStrings; + +export interface Props { + isWriteable: boolean; + toggleWriteable: () => void; + canUserWrite: boolean; + commit: (type: string, payload: any) => any; +} + +export const WorkpadHeader: FunctionComponent = ({ + isWriteable, + canUserWrite, + toggleWriteable, + commit, +}) => { + const keyHandler = (action: string) => { + if (action === 'EDITING') { + toggleWriteable(); + } + }; + + const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( + + {strings.getFullScreenTooltip()}{' '} + + + } + > + + + ); + + const getEditToggleToolTipText = () => { + if (!canUserWrite) { + return strings.getNoWritePermissionTooltipText(); + } + + const content = isWriteable + ? strings.getHideEditControlTooltip() + : strings.getShowEditControlTooltip(); + + return content; + }; + + const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { + const content = getEditToggleToolTipText(); + + if (textOnly) { + return content; + } + + return ( + + {content} + + ); + }; + + return ( + + + + {isWriteable && ( + + + + )} + + + + + + + + + + + + + + + {canUserWrite && ( + + )} + + + + + + + + + {fullscreenButton} + + + + + ); +}; + +WorkpadHeader.propTypes = { + isWriteable: PropTypes.bool, + toggleWriteable: PropTypes.func, + canUserWrite: PropTypes.bool, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx index eb4b451896b46..1f630040b0c36 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -4,147 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -// @ts-expect-error no @types definition -import { Shortcuts } from 'react-shortcuts'; -import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; -import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { RefreshControl } from './refresh_control'; -// @ts-expect-error untyped local -import { FullscreenControl } from './fullscreen_control'; -import { EditMenu } from './edit_menu'; -import { ElementMenu } from './element_menu'; -import { ShareMenu } from './share_menu'; -import { ViewMenu } from './view_menu'; - -const { WorkpadHeader: strings } = ComponentStrings; - -export interface Props { +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; +import { setWriteable } from '../../state/actions/workpad'; +import { State } from '../../../types'; +import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component'; + +interface StateProps { isWriteable: boolean; - toggleWriteable: () => void; canUserWrite: boolean; - commit: (type: string, payload: any) => any; + selectedPage: string; } -export const WorkpadHeader: FunctionComponent = ({ - isWriteable, - canUserWrite, - toggleWriteable, - commit, -}) => { - const keyHandler = (action: string) => { - if (action === 'EDITING') { - toggleWriteable(); - } - }; - - const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( - - {strings.getFullScreenTooltip()}{' '} - - - } - > - - - ); - - const getEditToggleToolTipText = () => { - if (!canUserWrite) { - return strings.getNoWritePermissionTooltipText(); - } - - const content = isWriteable - ? strings.getHideEditControlTooltip() - : strings.getShowEditControlTooltip(); - - return content; - }; - - const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { - const content = getEditToggleToolTipText(); - - if (textOnly) { - return content; - } - - return ( - - {content} - - ); - }; - - return ( - - - - {isWriteable && ( - - - - )} - - - - - - - - - - - - - - - {canUserWrite && ( - - )} - - - - - - - - - {fullscreenButton} - - - - - ); -}; +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; +} -WorkpadHeader.propTypes = { - isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, - canUserWrite: PropTypes.bool, -}; +const mapStateToProps = (state: State): StateProps => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + canUserWrite: canUserWrite(state), + selectedPage: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => ({ + ...stateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), +}); + +export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/index.ts b/x-pack/plugins/canvas/public/index.ts index c587623f2a0bc..d9a11204ad046 100644 --- a/x-pack/plugins/canvas/public/index.ts +++ b/x-pack/plugins/canvas/public/index.ts @@ -17,8 +17,4 @@ export interface WithKibanaProps { }; } -export interface UseKibanaProps { - canvas: CanvasServices; -} - -export const plugin = (initializerContext: PluginInitializerContext) => new CanvasPlugin(); +export const plugin = (_initializerContext: PluginInitializerContext) => new CanvasPlugin(); diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 9bd86ef98f1e3..9f79e81369b6b 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -12,7 +12,7 @@ import React, { FC, ReactElement, } from 'react'; -import { CanvasServices, CanvasServiceProviders } from '.'; +import { CanvasServices, CanvasServiceProviders, services } from '.'; export interface WithServicesProps { services: CanvasServices; @@ -36,23 +36,22 @@ export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; export const withServices = (type: ComponentType) => { - const EnhancedType: FC = (props) => { - const services = useServices(); - return createElement(type, { ...props, services }); - }; + const EnhancedType: FC = (props) => + createElement(type, { ...props, services: useServices() }); return EnhancedType; }; export const ServicesProvider: FC<{ - providers: CanvasServiceProviders; + providers?: Partial; children: ReactElement; -}> = ({ providers, children }) => { +}> = ({ providers = {}, children }) => { + const specifiedProviders: CanvasServiceProviders = { ...services, ...providers }; const value = { - embeddables: providers.embeddables.getService(), - expressions: providers.expressions.getService(), - notify: providers.notify.getService(), - platform: providers.platform.getService(), - navLink: providers.navLink.getService(), + embeddables: specifiedProviders.embeddables.getService(), + expressions: specifiedProviders.expressions.getService(), + notify: specifiedProviders.notify.getService(), + platform: specifiedProviders.platform.getService(), + navLink: specifiedProviders.navLink.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 3937d7fc05544..41d12db3a1853 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -31,7 +31,6 @@ @import '../components/function_form/function_form'; @import '../components/layout_annotations/layout_annotations'; @import '../components/loading/loading'; -@import '../components/navbar/navbar'; @import '../components/page_manager/page_manager'; @import '../components/positionable/positionable'; @import '../components/shape_preview/shape_preview'; diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index beea1814b54d2..671de53d74407 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -24,7 +24,7 @@ const storybookOptions = { run( ({ log, flags }) => { - const { dll, clean, stats, site } = flags; + const { addon, dll, clean, stats, site } = flags; // Delete the existing DLL if we're cleaning or building. if (clean || dll) { @@ -81,13 +81,20 @@ run( return; } + // Build the addon + execa.sync('node', ['scripts/build'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + // Build site and exit if (site) { log.success('storybook: Generating Storybook site'); storybook({ ...storybookOptions, mode: 'static', - outputDir: path.resolve(__dirname, './../storybook'), + outputDir: path.resolve(__dirname, './../storybook/build'), }); return; } @@ -100,6 +107,14 @@ run( ...options, }); + if (addon) { + execa('node', ['scripts/build', '--watch'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + } + storybook({ ...storybookOptions, port: 9001, @@ -110,8 +125,9 @@ run( Storybook runner for Canvas. `, flags: { - boolean: ['dll', 'clean', 'stats', 'site'], + boolean: ['addon', 'dll', 'clean', 'stats', 'site'], help: ` + --addon Watch the addon source code for changes. --clean Forces a clean of the Storybook DLL and exits. --dll Cleans and builds the Storybook dependency DLL and exits. --stats Produces a Webpack stats file. diff --git a/x-pack/plugins/canvas/server/lib/build_bool_array.js b/x-pack/plugins/canvas/server/lib/build_bool_array.ts similarity index 66% rename from x-pack/plugins/canvas/server/lib/build_bool_array.js rename to x-pack/plugins/canvas/server/lib/build_bool_array.ts index f1cab93ceebbb..bd418394cf375 100644 --- a/x-pack/plugins/canvas/server/lib/build_bool_array.js +++ b/x-pack/plugins/canvas/server/lib/build_bool_array.ts @@ -5,10 +5,11 @@ */ import { getESFilter } from './get_es_filter'; +import { ExpressionValueFilter } from '../../types'; -const compact = (arr) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); +const compact = (arr: T[]) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); -export function buildBoolArray(canvasQueryFilterArray) { +export function buildBoolArray(canvasQueryFilterArray: ExpressionValueFilter[]) { return compact( canvasQueryFilterArray.map((clause) => { try { diff --git a/x-pack/plugins/canvas/server/lib/filters.js b/x-pack/plugins/canvas/server/lib/filters.js deleted file mode 100644 index afa58c7ee30c2..0000000000000 --- a/x-pack/plugins/canvas/server/lib/filters.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - TODO: This could be pluggable -*/ - -export function time(filter) { - if (!filter.column) { - throw new Error('column is required for Elasticsearch range filters'); - } - return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, - }; -} - -export function luceneQueryString(filter) { - return { - query_string: { - query: filter.query || '*', - }, - }; -} - -export function exactly(filter) { - return { - term: { - [filter.column]: { - value: filter.value, - }, - }, - }; -} diff --git a/x-pack/plugins/canvas/server/lib/filters.ts b/x-pack/plugins/canvas/server/lib/filters.ts new file mode 100644 index 0000000000000..9997640154e2c --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/filters.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FilterType, + ExpressionValueFilter, + CanvasTimeFilter, + CanvasLuceneFilter, + CanvasExactlyFilter, +} from '../../types'; + +/* + TODO: This could be pluggable +*/ + +const isTimeFilter = ( + maybeTimeFilter: ExpressionValueFilter +): maybeTimeFilter is CanvasTimeFilter => { + return maybeTimeFilter.filterType === FilterType.time; +}; +const isLuceneFilter = ( + maybeLuceneFilter: ExpressionValueFilter +): maybeLuceneFilter is CanvasLuceneFilter => { + return maybeLuceneFilter.filterType === FilterType.luceneQueryString; +}; +const isExactlyFilter = ( + maybeExactlyFilter: ExpressionValueFilter +): maybeExactlyFilter is CanvasExactlyFilter => { + return maybeExactlyFilter.filterType === FilterType.exactly; +}; + +export function time(filter: ExpressionValueFilter) { + if (!isTimeFilter(filter) || !filter.column) { + throw new Error('column is required for Elasticsearch range filters'); + } + return { + range: { + [filter.column]: { gte: filter.from, lte: filter.to }, + }, + }; +} + +export function luceneQueryString(filter: ExpressionValueFilter) { + if (!isLuceneFilter(filter)) { + throw new Error('Filter is not a lucene filter'); + } + return { + query_string: { + query: filter.query || '*', + }, + }; +} + +export function exactly(filter: ExpressionValueFilter) { + if (!isExactlyFilter(filter)) { + throw new Error('Filter is not an exactly filter'); + } + return { + term: { + [filter.column]: { + value: filter.value, + }, + }, + }; +} + +export const filters: Record = { + exactly, + time, + luceneQueryString, +}; diff --git a/x-pack/plugins/canvas/server/lib/get_es_filter.js b/x-pack/plugins/canvas/server/lib/get_es_filter.ts similarity index 75% rename from x-pack/plugins/canvas/server/lib/get_es_filter.js rename to x-pack/plugins/canvas/server/lib/get_es_filter.ts index 7c025ed8dee9b..acc222ecc376f 100644 --- a/x-pack/plugins/canvas/server/lib/get_es_filter.js +++ b/x-pack/plugins/canvas/server/lib/get_es_filter.ts @@ -10,11 +10,11 @@ filter is the abstracted canvas filter. */ -/*eslint import/namespace: ['error', { allowComputed: true }]*/ -import * as filters from './filters'; +import { filters } from './filters'; +import { ExpressionValueFilter } from '../../types'; -export function getESFilter(filter) { - if (!filters[filter.filterType]) { +export function getESFilter(filter: ExpressionValueFilter) { + if (!filter.filterType || !filters[filter.filterType]) { throw new Error(`Unknown filter type: ${filter.filterType}`); } diff --git a/x-pack/plugins/canvas/server/lib/normalize_type.js b/x-pack/plugins/canvas/server/lib/normalize_type.ts similarity index 89% rename from x-pack/plugins/canvas/server/lib/normalize_type.js rename to x-pack/plugins/canvas/server/lib/normalize_type.ts index fda2fbe631646..b684325aacba9 100644 --- a/x-pack/plugins/canvas/server/lib/normalize_type.js +++ b/x-pack/plugins/canvas/server/lib/normalize_type.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export function normalizeType(type) { - const normalTypes = { +export function normalizeType(type: string) { + const normalTypes: Record = { string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point'], number: [ 'float', diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.js b/x-pack/plugins/canvas/server/lib/query_es_sql.js deleted file mode 100644 index 442703b00ea3a..0000000000000 --- a/x-pack/plugins/canvas/server/lib/query_es_sql.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { map, zipObject } from 'lodash'; -import { buildBoolArray } from './build_bool_array'; -import { sanitizeName } from './sanitize_name'; -import { normalizeType } from './normalize_type'; - -export const queryEsSQL = (elasticsearchClient, { count, query, filter, timezone }) => - elasticsearchClient('transport.request', { - path: '/_sql?format=json', - method: 'POST', - body: { - query, - time_zone: timezone, - fetch_size: count, - client_id: 'canvas', - filter: { - bool: { - must: [{ match_all: {} }, ...buildBoolArray(filter)], - }, - }, - }, - }) - .then((res) => { - const columns = res.columns.map(({ name, type }) => { - return { name: sanitizeName(name), type: normalizeType(type) }; - }); - const columnNames = map(columns, 'name'); - const rows = res.rows.map((row) => zipObject(columnNames, row)); - - if (!!res.cursor) { - elasticsearchClient('transport.request', { - path: '/_sql/close', - method: 'POST', - body: { - cursor: res.cursor, - }, - }).catch((e) => { - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); - } - - return { - type: 'datatable', - columns, - rows, - }; - }) - .catch((e) => { - if (e.message.indexOf('parsing_exception') > -1) { - throw new Error( - `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` - ); - } - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts new file mode 100644 index 0000000000000..c3c122d1e301a --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { zipObject } from 'lodash'; +import { queryEsSQL } from './query_es_sql'; +// @ts-expect-error +import { buildBoolArray } from './build_bool_array'; + +const response = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [ + ['foo', 'bar'], + ['buz', 'baz'], + ], + cursor: 'cursor-value', +}; + +const baseArgs = { + count: 1, + query: 'query', + filter: [], + timezone: 'timezone', +}; + +const getApi = (resp = response) => { + const api = jest.fn(); + api.mockResolvedValue(resp); + return api; +}; + +describe('query_es_sql', () => { + it('should call the api with the given args', async () => { + const api = getApi(); + + queryEsSQL(api, baseArgs); + + expect(api).toHaveBeenCalled(); + const givenArgs = api.mock.calls[0][1]; + + expect(givenArgs.body.fetch_size).toBe(baseArgs.count); + expect(givenArgs.body.query).toBe(baseArgs.query); + expect(givenArgs.body.time_zone).toBe(baseArgs.timezone); + }); + + it('formats the response', async () => { + const api = getApi(); + + const result = await queryEsSQL(api, baseArgs); + + const expectedColumns = response.columns.map((c) => ({ name: c.name, type: 'string' })); + const columnNames = expectedColumns.map((c) => c.name); + const expectedRows = response.rows.map((r) => zipObject(columnNames, r)); + + expect(result.type).toBe('datatable'); + expect(result.columns).toEqual(expectedColumns); + expect(result.rows).toEqual(expectedRows); + }); + + it('fetches pages until it has the requested count', async () => { + const pageOne = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [['foo', 'bar']], + cursor: 'cursor-value', + }; + + const pageTwo = { + rows: [['buz', 'baz']], + }; + + const api = getApi(pageOne); + api.mockReturnValueOnce(pageOne).mockReturnValueOnce(pageTwo); + + const result = await queryEsSQL(api, { ...baseArgs, count: 2 }); + expect(result.rows).toHaveLength(2); + }); + + it('closes any cursors that remain open', async () => { + const api = getApi(); + + await queryEsSQL(api, baseArgs); + expect(api.mock.calls[1][1].body.cursor).toBe(response.cursor); + }); + + it('throws on errors', async () => { + const api = getApi(); + api.mockRejectedValueOnce(new Error('parsing_exception')); + api.mockRejectedValueOnce(new Error('generic es error')); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: parsing_exception"` + ); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unexpected error from Elasticsearch: generic es error"` + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.ts new file mode 100644 index 0000000000000..8639cfa31dca8 --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.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 { map, zipObject } from 'lodash'; +import { buildBoolArray } from './build_bool_array'; +import { sanitizeName } from './sanitize_name'; +import { normalizeType } from './normalize_type'; +import { LegacyAPICaller } from '../../../../../src/core/server'; +import { ExpressionValueFilter } from '../../types'; + +interface Args { + count: number; + query: string; + timezone?: string; + filter: ExpressionValueFilter[]; +} + +interface CursorResponse { + cursor?: string; + rows: string[][]; +} + +type QueryResponse = CursorResponse & { + columns: Array<{ + name: string; + type: string; + }>; + cursor?: string; + rows: string[][]; +}; + +export const queryEsSQL = async ( + elasticsearchClient: LegacyAPICaller, + { count, query, filter, timezone }: Args +) => { + try { + let response: QueryResponse = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + query, + time_zone: timezone, + fetch_size: count, + client_id: 'canvas', + filter: { + bool: { + must: [{ match_all: {} }, ...buildBoolArray(filter)], + }, + }, + }, + }); + + const columns = response.columns.map(({ name, type }) => { + return { name: sanitizeName(name), type: normalizeType(type) }; + }); + const columnNames = map(columns, 'name'); + let rows = response.rows.map((row) => zipObject(columnNames, row)); + + while (rows.length < count && response.cursor !== undefined) { + response = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + + rows = [...rows, ...response.rows.map((row) => zipObject(columnNames, row))]; + } + + if (response.cursor !== undefined) { + elasticsearchClient('transport.request', { + path: '/_sql/close', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + } + + return { + type: 'datatable', + columns, + rows, + }; + } catch (e) { + if (e.message.indexOf('parsing_exception') > -1) { + throw new Error( + `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` + ); + } + throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); + } +}; diff --git a/x-pack/plugins/canvas/server/lib/sanitize_name.js b/x-pack/plugins/canvas/server/lib/sanitize_name.ts similarity index 85% rename from x-pack/plugins/canvas/server/lib/sanitize_name.js rename to x-pack/plugins/canvas/server/lib/sanitize_name.ts index 4c787c816a331..781ab20509b36 100644 --- a/x-pack/plugins/canvas/server/lib/sanitize_name.js +++ b/x-pack/plugins/canvas/server/lib/sanitize_name.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function sanitizeName(name) { +export function sanitizeName(name: string) { // invalid characters const invalid = ['(', ')']; const pattern = invalid.map((v) => escapeRegExp(v)).join('|'); @@ -12,6 +12,6 @@ export function sanitizeName(name) { return name.replace(regex, '_'); } -function escapeRegExp(string) { +function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts index 7a9830124e305..000b7f6029952 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts @@ -8,7 +8,6 @@ import { mapValues, keys } from 'lodash'; import { schema } from '@kbn/config-schema'; import { API_ROUTE } from '../../../common/lib'; import { catchErrorHandler } from '../catch_error_handler'; -// @ts-expect-error unconverted lib import { normalizeType } from '../../lib/normalize_type'; import { RouteInitializerDeps } from '..'; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/page.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/page.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/rendered_element.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/rendered_element.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/canvas.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/canvas.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/page.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/page.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/rendered_element.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/rendered_element.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/scrubber.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/scrubber.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/footer.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/footer.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/page_controls.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/page_controls.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/title.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/title.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/autoplay_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/autoplay_settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/toolbar_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/toolbar_settings.stories.tsx diff --git a/x-pack/plugins/canvas/storybook/addon/babel.config.js b/x-pack/plugins/canvas/storybook/addon/babel.config.js new file mode 100644 index 0000000000000..5081cf455906f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/babel.config.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + presets: ['@kbn/babel-preset/webpack_preset'], + plugins: ['@babel/plugin-proposal-class-properties'], +}; diff --git a/x-pack/plugins/canvas/storybook/addon/scripts/build.js b/x-pack/plugins/canvas/storybook/addon/scripts/build.js new file mode 100644 index 0000000000000..b3525244fad25 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/scripts/build.js @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const { resolve } = require('path'); + +const del = require('del'); +const supportsColor = require('supports-color'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +const padRight = (width, str) => + str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; + +run( + async ({ log, flags }) => { + await withProcRunner(log, async (proc) => { + if (!flags.watch) { + log.info('Deleting old output'); + await del(BUILD_DIR); + } + + const cwd = ROOT_DIR; + const env = { process }; + + if (supportsColor.stdout) { + env.FORCE_COLOR = 'true'; + } + + log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); + await proc.run(padRight(10, `babel`), { + cmd: 'babel', + args: [ + 'src', + '--config-file', + require.resolve('../babel.config.js'), + '--out-dir', + BUILD_DIR, + '--extensions', + '.ts,.js,.tsx', + '--copy-files', + ...(flags.watch ? ['--watch'] : ['--quiet']), + ], + wait: true, + env, + cwd, + }); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for Canvas Storybook addon', + flags: { + boolean: ['watch'], + help: ` + --watch Run in watch mode + `, + }, + } +); diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx new file mode 100644 index 0000000000000..9c29a44a67318 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { EuiSelectable, EuiSelectableOption } from '@elastic/eui'; +import addons from '@storybook/addons'; +import uuid from 'uuid/v4'; + +import { EVENTS } from '../constants'; +import { RecordedAction, RecordedPayload } from '../types'; + +export const ActionList: FC<{ + onSelect: (action: RecordedAction | null) => void; +}> = ({ onSelect }) => { + const [recordedActions, setRecordedActions] = useState>({}); + const [selectedAction, setSelectedAction] = useState(null); + + useEffect(() => { + onSelect(selectedAction); + }, [onSelect, selectedAction]); + + useEffect(() => { + const actionListener = (newAction: RecordedPayload) => { + const id = uuid(); + setRecordedActions({ ...recordedActions, [id]: { ...newAction, id } }); + }; + + const resetListener = () => { + setSelectedAction(null); + setRecordedActions({}); + }; + + const channel = addons.getChannel(); + channel.addListener(EVENTS.ACTION, actionListener); + channel.addListener(EVENTS.RESET, resetListener); + + return () => { + channel.removeListener(EVENTS.ACTION, actionListener); + channel.removeListener(EVENTS.RESET, resetListener); + }; + }); + + useEffect(() => { + const values = Object.values(recordedActions); + if (values.length > 0) { + setSelectedAction(values[values.length - 1]); + } + }, [recordedActions]); + + const options: EuiSelectableOption[] = Object.values(recordedActions).map((recordedAction) => ({ + id: recordedAction.id, + key: recordedAction.id, + label: recordedAction.action.type, + checked: recordedAction.id === selectedAction?.id ? 'on' : undefined, + })); + + const onChange: (selectedOptions: EuiSelectableOption[]) => void = (selectedOptions) => { + selectedOptions.forEach((option) => { + if (option && option.checked && option.id) { + const selected = recordedActions[option.id]; + + if (selected) { + setSelectedAction(selected); + } + } + }); + }; + + return ( + + {(list) => list} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx new file mode 100644 index 0000000000000..351b94edb351f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { isObject, isDate } from 'lodash'; +import uuid from 'uuid/v4'; +import { EuiTreeView } from '@elastic/eui'; + +import { Node } from '@elastic/eui/src/components/tree_view/tree_view'; + +import { RecordedAction } from '../types'; + +const actionToTree = (recordedAction: RecordedAction) => { + const { action, newState, previousState } = recordedAction; + + return [ + { + label: 'Action', + id: uuid(), + children: jsonToTree(action), + }, + { + label: 'Previous State', + id: uuid(), + children: jsonToTree(previousState), + }, + { + label: 'Current State', + id: uuid(), + children: jsonToTree(newState), + }, + ]; +}; + +const jsonToTree: (obj: Record) => Node[] = (obj) => { + const keys = Object.keys(obj); + + const values = keys.map((label) => { + const value = obj[label]; + + if (!value) { + return null; + } + + const id = uuid(); + + if (isDate(value)) { + return { label: `${label}: ${(value as Date).toDateString()}` }; + } + + if (isObject(value)) { + const children = jsonToTree(value); + + if (children !== null && Object.keys(children).length > 0) { + return { label, id, children }; + } else { + return { label, id }; + } + } + + return { label: `${label}: ${value.toString().slice(0, 100)}`, id }; + }); + + return values.filter((value) => value !== null) as Node[]; +}; + +export const ActionTree: FC<{ action: RecordedAction | null }> = ({ action }) => { + const items = action ? actionToTree(action) : null; + let tree = <>; + + if (action && items) { + tree = ( + + ); + } else if (action) { + tree =
No change
; + } + + return tree; +}; diff --git a/x-pack/plugins/canvas/public/components/navbar/index.js b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts similarity index 66% rename from x-pack/plugins/canvas/public/components/navbar/index.js rename to x-pack/plugins/canvas/storybook/addon/src/components/index.ts index 6948ada93155d..5acb1acf3b459 100644 --- a/x-pack/plugins/canvas/public/components/navbar/index.js +++ b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts @@ -4,7 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Navbar as Component } from './navbar'; - -export const Navbar = pure(Component); +export { ActionList } from './action_list'; +export { ActionTree } from './action_tree'; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx new file mode 100644 index 0000000000000..4db3c23c93843 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx @@ -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. + */ + +import React, { FC } from 'react'; +import { EuiAccordion } from '@elastic/eui'; +import { formatters } from 'jsondiffpatch'; + +import { RecordedAction } from '../types'; + +interface Props { + action: RecordedAction | null; +} + +export const StateChange: FC = ({ action }) => { + if (!action) { + return null; + } + + const { change, previousState } = action; + const html = formatters.html.format(change, previousState); + formatters.html.hideUnchanged(); + + return ( + + {/* eslint-disable-next-line react/no-danger */} +
+ + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/constants.ts b/x-pack/plugins/canvas/storybook/addon/src/constants.ts new file mode 100644 index 0000000000000..fb2646ef3ba8f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/constants.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 const ADDON_ID = 'kbn-canvas/redux-actions'; +export const ACTIONS_PANEL_ID = `${ADDON_ID}/panel`; + +const RESULT = `${ADDON_ID}/result`; +const REQUEST = `${ADDON_ID}/request`; +const ACTION = `${ADDON_ID}/action`; +const RESET = `${ADDON_ID}/reset`; + +export const EVENTS = { ACTION, RESULT, REQUEST, RESET }; diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.css b/x-pack/plugins/canvas/storybook/addon/src/panel.css new file mode 100644 index 0000000000000..b2b6591343b5f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.css @@ -0,0 +1,171 @@ +.panel__tree { + font-family: monospace; + font-size: 85%; +} + +.panel__tree .euiTreeView { + padding-left: 12px; + font-size: 85%; +} + +.panel__resizeableContainer { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; +} + +.panel__stateChange .euiAccordion__button { + font-size: 12px; + font-family: monospace; +} + +.panel__stateChange .euiAccordion__iconWrapper { + transform: scale(.80); + transform-origin: top left; + margin: 8px 0px 4px 7px; +} + +.jsondiffpatch-delta { + font-family: monospace; + font-size: 12px; + line-height: 20px; + margin: 0; + padding: 0 0 0 12px; + display: inline-block; +} +.jsondiffpatch-delta pre { + font-size: 12px; + margin: 0; + padding: 0; + display: inline-block; +} +ul.jsondiffpatch-delta { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} +.jsondiffpatch-delta ul { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} + +.jsondiffpatch-added .jsondiffpatch-property-name, +.jsondiffpatch-added .jsondiffpatch-value pre, +.jsondiffpatch-modified .jsondiffpatch-right-value pre, +.jsondiffpatch-textdiff-added { + background: #bbffbb; +} + +.jsondiffpatch-deleted .jsondiffpatch-property-name, +.jsondiffpatch-deleted pre, +.jsondiffpatch-modified .jsondiffpatch-left-value pre, +.jsondiffpatch-textdiff-deleted { + background: #ffbbbb; + text-decoration: line-through; +} + +.jsondiffpatch-unchanged { display: none; } + +.jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-property-name { + display: inline-block; + padding-right: 5px; + vertical-align: top; +} + +.jsondiffpatch-property-name:after { + content: ': '; +} + +.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { + content: ': ['; +} + +.jsondiffpatch-child-node-type-array:after { + content: '],'; +} + +div.jsondiffpatch-child-node-type-array:before { + content: '['; +} + +div.jsondiffpatch-child-node-type-array:after { + content: ']'; +} + +.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { + content: ': {'; +} + +.jsondiffpatch-child-node-type-object:after { + content: '},'; +} + +div.jsondiffpatch-child-node-type-object:before { + content: '{'; +} + +div.jsondiffpatch-child-node-type-object:after { + content: '}'; +} + +.jsondiffpatch-value pre:after { + content: ','; +} + +li:last-child > .jsondiffpatch-value pre:after, +.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { + content: ''; +} + +.jsondiffpatch-modified .jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-modified .jsondiffpatch-right-value { + margin-left: 5px; +} + +.jsondiffpatch-moved .jsondiffpatch-value { + display: none; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination { + display: inline-block; + background: #ffffbb; + color: #888; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination:before { + content: ' => '; +} + +ul.jsondiffpatch-textdiff { + padding: 0; +} + +.jsondiffpatch-textdiff-location { + color: #bbb; + display: inline-block; + min-width: 60px; +} + +.jsondiffpatch-textdiff-line { + display: inline-block; +} + +.jsondiffpatch-textdiff-line-number:after { + content: ','; +} + +.jsondiffpatch-error { + background: red; + color: white; + font-weight: bold; +} diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.tsx b/x-pack/plugins/canvas/storybook/addon/src/panel.tsx new file mode 100644 index 0000000000000..adf6e8555c00a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiResizableContainer } from '@elastic/eui'; +import { StateChange } from './components/state_change'; + +import '@elastic/eui/dist/eui_theme_light.css'; +import './panel.css'; + +import { RecordedAction } from './types'; +import { ActionList, ActionTree } from './components'; + +export const Panel = () => { + const [selectedAction, setSelectedAction] = useState(null); + + return ( + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/register.tsx b/x-pack/plugins/canvas/storybook/addon/src/register.tsx new file mode 100644 index 0000000000000..3a5c4a6818ac1 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/register.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { addons, types } from '@storybook/addons'; +import { AddonPanel } from '@storybook/components'; +import { STORY_CHANGED } from '@storybook/core-events'; + +import { ADDON_ID, EVENTS, ACTIONS_PANEL_ID } from './constants'; +import { Panel } from './panel'; + +addons.register(ADDON_ID, (api) => { + const channel = addons.getChannel(); + + api.on(STORY_CHANGED, (storyId) => { + channel.emit(EVENTS.RESET, storyId); + }); + + addons.add(ACTIONS_PANEL_ID, { + title: 'Redux Actions', + type: types.PANEL, + render: ({ active, key }) => { + return ( + + + + ); + }, + }); +}); diff --git a/x-pack/plugins/canvas/storybook/addon/src/state.ts b/x-pack/plugins/canvas/storybook/addon/src/state.ts new file mode 100644 index 0000000000000..6d601fff7184a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/state.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ +import { applyMiddleware, Dispatch, Store } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import addons from '@storybook/addons'; +import { diff } from 'jsondiffpatch'; +import { isFunction } from 'lodash'; + +import { EVENTS } from './constants'; + +// @ts-expect-error untyped local +import { appReady } from '../../../public/state/middleware/app_ready'; +// @ts-expect-error untyped local +import { resolvedArgs } from '../../../public/state/middleware/resolved_args'; + +// @ts-expect-error untyped local +import { getRootReducer } from '../../../public/state/reducers'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../../public/state/defaults'; +// @ts-expect-error Untyped local +import { getInitialState as getState } from '../../../public/state/initial_state'; +import { State } from '../../../types'; + +export const getInitialState: () => State = () => getState(); +export const getMiddleware = () => applyMiddleware(thunkMiddleware); +export const getReducer = () => getRootReducer(getInitialState()); + +export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( + action +) => { + const channel = addons.getChannel(); + + const previousState = store.getState(); + const returnValue = dispatch(action); + const newState = store.getState(); + const change = diff(previousState, newState) || {}; + + channel.emit(EVENTS.ACTION, { + previousState, + newState, + change, + action: isFunction(action) ? { type: '(thunk)' } : action, + }); + + return returnValue; +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/types.ts b/x-pack/plugins/canvas/storybook/addon/src/types.ts new file mode 100644 index 0000000000000..e8a2cb70c89ff --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { State } from '../../../types'; + +export interface RecordedPayload { + previousState: State; + newState: State; + change: Partial; + action: Action; +} + +export interface RecordedAction extends RecordedPayload { + id: string; +} diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json new file mode 100644 index 0000000000000..9cab0af235f2e --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.json", + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "target" + ], + "compilerOptions": { + "declaration": false, + } +} diff --git a/x-pack/plugins/canvas/storybook/config.js b/x-pack/plugins/canvas/storybook/config.js deleted file mode 100644 index dc16d6c46084d..0000000000000 --- a/x-pack/plugins/canvas/storybook/config.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { configure, addDecorator, addParameters } from '@storybook/react'; -import { withInfo } from '@storybook/addon-info'; -import { create } from '@storybook/theming'; - -import { startServices } from '../public/services/stubs'; -import { addDecorators } from './decorators'; - -// If we're running Storyshots, be sure to register the require context hook. -// Otherwise, add the other decorators. -if (process.env.NODE_ENV === 'test') { - require('babel-plugin-require-context-hook/register')(); -} else { - // Customize the info for each story. - addDecorator( - withInfo({ - inline: true, - styles: { - infoBody: { - margin: 20, - }, - infoStory: { - margin: '40px 60px', - }, - }, - }) - ); -} - -addDecorators(); -startServices(); - -function loadStories() { - require('./dll_contexts'); - - // Only gather and require CSS files related to Canvas. The other CSS files - // are built into the DLL. - const css = require.context( - '../../../../built_assets/css', - true, - /plugins\/(?=canvas).*light\.css/ - ); - css.keys().forEach((filename) => css(filename)); - - // Find all files ending in *.stories.tsx - const req = require.context('./..', true, /.(stories).tsx$/); - req.keys().forEach((filename) => req(filename)); - - // Import Canvas CSS - require('../public/style/index.scss'); -} - -// Set up the Storybook environment with custom settings. -addParameters({ - options: { - theme: create({ - base: 'light', - brandTitle: 'Canvas Storybook', - brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', - }), - showPanel: true, - isFullscreen: false, - panelPosition: 'bottom', - isToolshown: true, - }, -}); - -configure(loadStories, module); diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts index aa1e958a410f5..8cd716cf7e3f1 100644 --- a/x-pack/plugins/canvas/storybook/decorators/index.ts +++ b/x-pack/plugins/canvas/storybook/decorators/index.ts @@ -5,15 +5,43 @@ */ import { addDecorator } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; // @ts-expect-error import { withInfo } from '@storybook/addon-info'; +import { Provider as ReduxProvider } from 'react-redux'; + +import { ServicesProvider } from '../../public/services'; +import { RouterContext } from '../../public/components/router'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { routerContextDecorator } from './router_decorator'; import { kibanaContextDecorator } from './kibana_decorator'; +import { servicesContextDecorator } from './services_decorator'; + +export { reduxDecorator } from './redux_decorator'; export const addDecorators = () => { - addDecorator(withKnobs); + if (process.env.NODE_ENV === 'test') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('babel-plugin-require-context-hook/register')(); + } else { + // Customize the info for each story. + addDecorator( + withInfo({ + inline: true, + styles: { + infoBody: { + margin: 20, + }, + infoStory: { + margin: '40px 60px', + }, + }, + propTablesExclude: [ReduxProvider, ServicesProvider, RouterContext, KibanaContextProvider], + }) + ); + } + addDecorator(kibanaContextDecorator); addDecorator(routerContextDecorator); + addDecorator(servicesContextDecorator); }; diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx new file mode 100644 index 0000000000000..e35b065a61764 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { createStore } from 'redux'; +import { Provider as ReduxProvider } from 'react-redux'; +import { cloneDeep } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../public/state/defaults'; +import { CanvasWorkpad, CanvasElement, CanvasAsset } from '../../types'; + +// @ts-expect-error untyped local +import { elementsRegistry } from '../../public/lib/elements_registry'; +import { image } from '../../canvas_plugin_src/elements/image'; +elementsRegistry.register(image); + +import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state'; +export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants'; + +interface Params { + workpad?: CanvasWorkpad; + elements?: CanvasElement[]; + assets?: CanvasAsset[]; +} + +export const reduxDecorator = (params: Params = {}) => { + const state = cloneDeep(getInitialState()); + const { workpad, elements, assets } = params; + + if (workpad) { + set(state, 'persistent.workpad', workpad); + } + + if (elements) { + set(state, 'persistent.workpad.pages.0.elements', elements); + } + + if (assets) { + set( + state, + 'assets', + assets.reduce((obj: Record, item) => { + obj[item.id] = item; + return obj; + }, {}) + ); + } + + return (story: Function) => { + const store = createStore(getReducer(), state, getMiddleware()); + store.dispatch = patchDispatch(store, store.dispatch); + return {story()}; + }; +}; diff --git a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx index 43b0da6473f23..464577b1f7c1e 100644 --- a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx @@ -5,26 +5,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -class RouterContext extends React.Component { - static childContextTypes = { - router: PropTypes.object.isRequired, - }; +import { RouterContext } from '../../public/components/router'; - getChildContext() { - return { - router: { - getFullPath: () => 'path', - create: () => '', - }, - }; - } - render() { - return <>{this.props.children}; - } -} - -export function routerContextDecorator(story: Function) { - return {story()}; -} +export const routerContextDecorator = (story: Function) => ( + {} }}>{story()} +); diff --git a/x-pack/plugins/canvas/public/components/navbar/navbar.js b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx similarity index 58% rename from x-pack/plugins/canvas/public/components/navbar/navbar.js rename to x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx index dcf6389acd4a3..918eaffb47d77 100644 --- a/x-pack/plugins/canvas/public/components/navbar/navbar.js +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -5,12 +5,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -export const Navbar = ({ children }) => { - return
{children}
; -}; +import { ServicesProvider } from '../../public/services'; -Navbar.propTypes = { - children: PropTypes.node, -}; +export const servicesContextDecorator = (story: Function) => ( + {story()} +); diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts new file mode 100644 index 0000000000000..5cad89eb614e5 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ACTIONS_PANEL_ID } from './addon/src/constants'; + +export * from './decorators'; +export { ACTIONS_PANEL_ID } from './addon/src/constants'; +export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } }); diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts new file mode 100644 index 0000000000000..ad6d10f9bc75f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + stories: ['../**/*.stories.tsx'], + addons: [ + '@storybook/addon-actions', + '@storybook/addon-knobs', + './storybook/addon/target/register', + ], +}; diff --git a/x-pack/plugins/canvas/storybook/manager.ts b/x-pack/plugins/canvas/storybook/manager.ts new file mode 100644 index 0000000000000..6727040c9b27f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/manager.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Canvas Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', + }), + showPanel: true, + isFullscreen: false, + panelPosition: 'bottom', + isToolshown: true, + selectedPanel: PANEL_ID, +}); diff --git a/x-pack/plugins/canvas/storybook/middleware.js b/x-pack/plugins/canvas/storybook/middleware.ts similarity index 74% rename from x-pack/plugins/canvas/storybook/middleware.js rename to x-pack/plugins/canvas/storybook/middleware.ts index baa524aefa709..d319a6918a02a 100644 --- a/x-pack/plugins/canvas/storybook/middleware.js +++ b/x-pack/plugins/canvas/storybook/middleware.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -const path = require('path'); -const serve = require('serve-static'); +import path from 'path'; +// @ts-expect-error +import serve from 'serve-static'; // Extend the Storybook Middleware to include a route to access Legacy UI assets -module.exports = function (router) { +module.exports = function (router: { get: (...args: any[]) => void }) { router.get( '/ui', serve(path.resolve(__dirname, '../../../../../src/core/server/core_app/assets')) diff --git a/x-pack/plugins/canvas/storybook/preview.ts b/x-pack/plugins/canvas/storybook/preview.ts new file mode 100644 index 0000000000000..fc194664c84b8 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/preview.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; + +import { startServices } from '../public/services/stubs'; +import { addDecorators } from './decorators'; + +// Import the modules from the DLL. +import './dll_contexts'; + +// Import Canvas CSS +import '../public/style/index.scss'; + +startServices({ + notify: { + success: (message) => action(`success: ${message}`)(), + error: (message) => action(`error: ${message}`)(), + info: (message) => action(`info: ${message}`)(), + warning: (message) => action(`warning: ${message}`)(), + }, +}); + +addDecorators(); + +// Only gather and require CSS files related to Canvas. The other CSS files +// are built into the DLL. +const css = require.context( + '../../../../built_assets/css', + true, + /plugins\/(?=canvas).*light\.css/ +); +css.keys().forEach((filename) => css(filename)); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.js b/x-pack/plugins/canvas/storybook/storyshots.test.tsx similarity index 77% rename from x-pack/plugins/canvas/storybook/storyshots.test.js rename to x-pack/plugins/canvas/storybook/storyshots.test.tsx index e3a9654bb49fa..85ec7baf18c62 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -4,18 +4,23 @@ * 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'; import 'moment-timezone'; import ReactDOM from 'react-dom'; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; +// @ts-expect-error untyped library import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; import { addSerializer } from 'jest-specific-snapshot'; // Several of the renderers, used by the runtime, use jQuery. import jquery from 'jquery'; +// @ts-expect-error jQuery global global.$ = jquery; +// @ts-expect-error jQuery global global.jQuery = jquery; // Set our default timezone to UTC for tests so we can generate predictable snapshots @@ -23,7 +28,7 @@ moment.tz.setDefault('UTC'); // Freeze time for the tests for predictable snapshots const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 -Date.now = jest.fn(() => testTime); +Date.now = jest.fn(() => testTime.getTime()); // Mock telemetry service jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => {} })); @@ -53,10 +58,10 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }); // Mock React Portal for components that use modals, tooltips, etc -ReactDOM.createPortal = jest.fn((element) => { - return element; -}); +// @ts-expect-error Portal mocks are notoriously difficult to type +ReactDOM.createPortal = jest.fn((element) => element); +// Mock the EUI HTML ID Generator so elements have a predictable ID in snapshots jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { htmlIdGenerator: () => () => `generated-id`, @@ -67,18 +72,19 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { // https://github.com/elastic/eui/issues/3712 jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { return { - EuiOverlayMask: ({ children }) => children, + EuiOverlayMask: ({ children }: { children: ReactChildren }) => children, }; }); // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories', () => { return 'Disabled Panel'; } ); +// @ts-expect-error untyped library import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; jest.mock('@elastic/eui/test-env/components/observer/observer'); EuiObserver.mockImplementation(() => 'EuiObserver'); @@ -86,13 +92,21 @@ EuiObserver.mockImplementation(() => 'EuiObserver'); // This element uses a `ref` and cannot be rendered by Jest snapshots. import { RenderedElement } from '../shareable_runtime/components/rendered_element'; 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 initStoryshots({ configPath: path.resolve(__dirname, './../storybook'), test: multiSnapshotWithOptions({}), + // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, }); diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 982185a731b14..1321ade30bbde 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -6,236 +6,198 @@ const path = require('path'); const webpack = require('webpack'); +const webpackMerge = require('webpack-merge'); const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); // 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')], - }, - }); - - // Handle Typescript files - config.module.rules.push({ - test: /\.tsx?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }, - ], - }); - - config.module.rules.push({ - test: /\.mjs$/, - include: /node_modules/, - type: 'javascript/auto', - }); - - // 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 - // - // 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, but exclude CSS Modules in Storybook - 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'), - }, - }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') - )};\n`; - }, - sassOptions: { - includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], +module.exports = async ({ config: storybookConfig }) => { + const 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')], }, }, - }, - ], - }); - - // Enable CSS Modules in Storybook - config.module.rules.push({ - test: /\.module\.s(a|c)ss$/, - loader: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - modules: { - localIdentName: '[name]__[local]___[hash:base64:5]', - }, + // 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: /\.examples.tsx$/, + // use: [ + // // Parse TS comments to create Props tables in the UI + // require.resolve('react-docgen-typescript-loader'), + // ], + // }, + // Enable SASS, but exclude CSS Modules in Storybook + { + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') + )};\n`; + }, + sassOptions: { + includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], + }, + }, + }, + ], }, - }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), - }, + // Enable CSS Modules in Storybook (Shareable Runtime) + { + test: /\.module\.s(a|c)ss$/, + loader: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + }, + ], }, - }, - { - loader: 'sass-loader', - }, - ], - }); - - // Exclude large-dependency modules that need not be included in Storybook. - config.module.rules.push({ - test: [ - path.resolve(__dirname, '../public/components/embeddable_flyout'), - path.resolve(__dirname, '../../reporting/public'), - ], - use: 'null-loader', - }); - - // Ensure jQuery is global for Storybook, specifically for the runtime. - config.plugins.push( - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - }) - ); - - // 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: path.resolve(DLL_OUTPUT, 'manifest.json'), - context: KIBANA_ROOT, - }) - ); - - // Copy the DLL files to the Webpack build for use in the Storybook UI - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ { - from: path.resolve(DLL_OUTPUT, 'dll.js'), - to: 'dll.js', + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', }, + // Exclude large-dependency, troublesome or irrelevant modules. { - from: path.resolve(DLL_OUTPUT, 'dll.css'), - to: 'dll.css', + test: [ + path.resolve(__dirname, '../public/components/embeddable_flyout'), + path.resolve(__dirname, '../../reporting/public'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/angular'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/paginate'), + ], + use: 'null-loader', }, ], - }) - ); - - config.plugins.push( - // replace imports for `uiExports/*` modules with a synthetic module - // created by create_ui_exports_module.js - new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { - // uiExports used by Canvas - const extensions = { - hacks: [], - chromeNavControls: [], - }; - - // everything following the first / in the request is - // treated as a type of appExtension - const type = resource.request.slice(resource.request.indexOf('/') + 1); - - resource.request = [ - // the "val-loader" is used to execute create_ui_exports_module - // and use its return value as the source for the module in the - // bundle. This allows us to bypass writing to the file system - require.resolve('val-loader'), - '!', - require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), - '?', - // this JSON is parsed by create_ui_exports_module and determines - // what require() calls it will execute within the bundle - JSON.stringify({ type, modules: extensions[type] || [] }), - ].join(''); - }), - - // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/notify/, - path.resolve(__dirname, '../tasks/mocks/uiNotify') - ), - new webpack.NormalModuleReplacementPlugin( - /lib\/download_workpad/, - path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/custom_element_service/, - path.resolve(__dirname, '../tasks/mocks/customElementService') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/ui_metric/, - path.resolve(__dirname, '../tasks/mocks/uiMetric') - ) - ); - - // Tell Webpack about relevant extensions - config.resolve.extensions.push('.ts', '.tsx', '.scss'); - - // Alias imports to either a mock or the proper module or directory. - // NOTE: order is important here - `ui/notify` will override `ui/notify/foo` if it - // is added first. - config.resolve.alias['ui/notify/lib/format_msg'] = path.resolve( - __dirname, - '../tasks/mocks/uiNotifyFormatMsg' - ); - config.resolve.alias['ui/notify'] = path.resolve(__dirname, '../tasks/mocks/uiNotify'); - config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve( - __dirname, - '../tasks/mocks/uiAbsoluteToParsedUrl' - ); - config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); - config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); - config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); + }, + plugins: [ + // Reference the built DLL file of static(ish) dependencies, which are removed + // during kbn:bootstrap and rebuilt if missing. + new webpack.DllReferencePlugin({ + manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), + context: KIBANA_ROOT, + }), + // Ensure jQuery is global for Storybook, specifically for the runtime. + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery', + }), + // Copy the DLL files to the Webpack build for use in the Storybook UI + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(DLL_OUTPUT, 'dll.js'), + to: 'dll.js', + }, + { + from: path.resolve(DLL_OUTPUT, 'dll.css'), + to: 'dll.css', + }, + ], + }), + // replace imports for `uiExports/*` modules with a synthetic module + // created by create_ui_exports_module.js + new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { + // uiExports used by Canvas + const extensions = { + hacks: [], + chromeNavControls: [], + }; + + // everything following the first / in the request is + // treated as a type of appExtension + const type = resource.request.slice(resource.request.indexOf('/') + 1); + + resource.request = [ + // the "val-loader" is used to execute create_ui_exports_module + // and use its return value as the source for the module in the + // bundle. This allows us to bypass writing to the file system + require.resolve('val-loader'), + '!', + require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), + '?', + // this JSON is parsed by create_ui_exports_module and determines + // what require() calls it will execute within the bundle + JSON.stringify({ type, modules: extensions[type] || [] }), + ].join(''); + }), + + new webpack.NormalModuleReplacementPlugin( + /lib\/download_workpad/, + path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/custom_element_service/, + path.resolve(__dirname, '../tasks/mocks/customElementService') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/ui_metric/, + path.resolve(__dirname, '../tasks/mocks/uiMetric') + ), + ], + resolve: { + extensions: ['.ts', '.tsx', '.scss', '.mjs', '.html'], + alias: { + 'ui/url/absolute_to_parsed_url': path.resolve( + __dirname, + '../tasks/mocks/uiAbsoluteToParsedUrl' + ), + ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), + ng_mock$: path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'), + }, + }, + }; - config.resolve.extensions.push('.mjs'); + // 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', + }, + }); - return config; + return webpackMerge(storybookConfig, config); }; diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 81d19c035075f..4e54750f08eea 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -25,9 +25,6 @@ module.exports = { '@elastic/eui/dist/eui_theme_light.css', '@kbn/ui-framework/dist/kui_light.css', '@storybook/addon-actions/register', - '@storybook/addon-knobs', - '@storybook/addon-knobs/react', - '@storybook/addon-knobs/register', '@storybook/core', '@storybook/core/dist/server/common/polyfills.js', '@storybook/react', @@ -38,6 +35,7 @@ module.exports = { 'chroma-js', 'highlight.js', 'html-entities', + 'jsondiffpatch', 'jquery', 'lodash', 'markdown-it', diff --git a/x-pack/plugins/canvas/types/filters.ts b/x-pack/plugins/canvas/types/filters.ts new file mode 100644 index 0000000000000..356ebbbb76ac0 --- /dev/null +++ b/x-pack/plugins/canvas/types/filters.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExpressionValueFilter } from '.'; + +export enum FilterType { + luceneQueryString = 'luceneQueryString', + time = 'time', + exactly = 'exactly', +} + +export type CanvasTimeFilter = ExpressionValueFilter & { + filterType: typeof FilterType.time; + to: string; + from: string; +}; + +export type CanvasLuceneFilter = ExpressionValueFilter & { + filterType: typeof FilterType.luceneQueryString; + query: string; +}; + +export type CanvasExactlyFilter = ExpressionValueFilter & { + filterType: typeof FilterType.exactly; + value: string; + column: string; +}; + +export type CanvasFilter = CanvasTimeFilter | CanvasExactlyFilter | CanvasLuceneFilter; diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 0799627ce9b5a..f39c2d4367f9e 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -8,6 +8,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './filters'; export * from './functions'; export * from './renderers'; export * from './shortcuts'; diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index f0baa84afca32..637af39339e27 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 9bd1ffddeaca8..639f56d0cafca 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 d1ed410065248..927dc91f365b7 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 9c3a0edf7e733..0e9731a414119 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 faa4f2ee499e5..4fd1e889ba1a5 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 358335a2a4d60..d2a8384b1f882 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/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 434d38c76d428..4ddcb3386f314 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; -import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public'; @@ -18,7 +17,6 @@ export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; export interface PluginDeps { discover: Pick; - embeddable: Pick; kibanaLegacy?: { dashboardConfig: { getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls']; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 14cd48ae1f509..b6bdafc26b445 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -8,19 +8,14 @@ import { ExploreDataChartAction } from './explore_data_chart_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; -import { - EmbeddableStart, - RangeSelectContext, - ValueClickContext, - ChartActionContext, -} from '../../../../../../src/plugins/embeddable/public'; +import { ExploreDataChartActionContext } from './explore_data_chart_action'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, VISUALIZE_EMBEDDABLE_TYPE, } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { Filter, TimeRange } from '../../../../../../src/plugins/data/public'; +import { Filter, RangeFilter } from '../../../../../../src/plugins/data/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -34,10 +29,19 @@ afterEach(() => { i18nTranslateSpy.mockClear(); }); -const setup = ({ - useRangeEvent = false, - dashboardOnlyMode = false, -}: { useRangeEvent?: boolean; dashboardOnlyMode?: boolean } = {}) => { +const setup = ( + { + useRangeEvent = false, + timeFieldName, + filters = [], + dashboardOnlyMode = false, + }: { + useRangeEvent?: boolean; + filters?: Filter[]; + timeFieldName?: string; + dashboardOnlyMode?: boolean; + } = { filters: [] } +) => { type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; const core = coreMock.createStart(); @@ -46,17 +50,10 @@ const setup = ({ createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, @@ -91,19 +88,13 @@ const setup = ({ getOutput: () => output, } as unknown) as VisualizeEmbeddableContract; - const data: ChartActionContext['data'] = { - ...(useRangeEvent - ? ({ range: {} } as RangeSelectContext['data']) - : ({ data: [] } as ValueClickContext['data'])), - timeFieldName: 'order_date', - }; - const context = { + filters, + timeFieldName, embeddable, - data, - } as ChartActionContext; + } as ExploreDataChartActionContext; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context }; + return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -236,32 +227,41 @@ describe('"Explore underlying data" panel action', () => { }); test('applies chart event filters', async () => { - const { action, context, urlGenerator, plugins } = setup(); - - ((plugins.embeddable - .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => { - const filters: Filter[] = [ - { - meta: { - alias: 'alias', - disabled: false, - negate: false, + const timeFieldName = 'timeField'; + const from = '2020-07-13T13:40:43.583Z'; + const to = '2020-07-13T13:44:43.583Z'; + const filters: Array = [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + field: timeFieldName, + params: { + gte: from, + lte: to, }, }, - ]; - const timeRange: TimeRange = { - from: 'from', - to: 'to', - }; - return { filters, timeRange }; - }); + range: { + [timeFieldName]: { + gte: from, + lte: to, + }, + }, + }, + ]; - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0); + const { action, context, urlGenerator } = setup({ filters, timeFieldName }); await action.getHref(context); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context); expect(urlGenerator.createUrl).toHaveBeenCalledWith({ filters: [ { @@ -274,8 +274,8 @@ describe('"Explore underlying data" panel action', () => { ], indexPatternId: 'index-ptr-foo', timeRange: { - from: 'from', - to: 'to', + from, + to, }, }); }); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 658a6bcb3cf4d..a89fe3cd12a19 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -5,17 +5,19 @@ */ import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { - ValueClickContext, - RangeSelectContext, -} from '../../../../../../src/plugins/embeddable/public'; import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; -import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { + isTimeRange, + isQuery, + isFilters, + ApplyGlobalFilterActionContext, + esFilters, +} from '../../../../../../src/plugins/data/public'; import { KibanaURL } from './kibana_url'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; -export type ExploreDataChartActionContext = ValueClickContext | RangeSelectContext; +export type ExploreDataChartActionContext = ApplyGlobalFilterActionContext; export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; @@ -31,6 +33,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction { + if (context.embeddable?.type === 'map') return false; // TODO: https://github.com/elastic/kibana/issues/73043 + return super.isCompatible(context); + } + protected readonly getUrl = async ( context: ExploreDataChartActionContext ): Promise => { @@ -42,7 +49,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index 4b018354aa092..9e66925132a7d 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -9,8 +9,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { UiActionsSetup, UiActionsStart, - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, + APPLY_FILTER_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; @@ -77,8 +76,7 @@ export class DiscoverEnhancedPlugin if (this.config.actions.exploreDataInChart.enabled) { const exploreDataChartAction = new ExploreDataChartAction(params); - uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction); - uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction); + uiActions.addTriggerAction(APPLY_FILTER_TRIGGER, exploreDataChartAction); } } } 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 d5b50fce38718..9d28ef71a5518 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 e0e293b1cc3e7..81f52f986cab8 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 2e06ee55189d9..83fe233553351 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 dec8eaae56f41..710011d3bdf3e 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/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index 04aeba41fa00d..ca4fc0abc37a4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -157,6 +157,25 @@ describe('createTSVBLink()', () => { }); }); + it('should use the workaround index pattern when there are multiple listed in the source', () => { + const customSource = { + ...source, + metricAlias: 'my-beats-*,metrics-*', + fields: { ...source.fields, timestamp: 'time' }, + }; + const link = createTSVBLink(customSource, options, series, timeRange, chartOptions); + expect(link).toStrictEqual({ + app: 'visualize', + hash: '/create', + search: { + _a: + "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', + type: 'metrics', + }, + }); + }); + test('createFilterFromOptions()', () => { const customOptions = { ...options, groupBy: 'host.name' }; const customSeries = { ...series, id: 'test"foo' }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 3afc0d050e736..afddaf6621f10 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -23,6 +23,14 @@ import { SourceQuery } from '../../../../../graphql/types'; import { createMetricLabel } from './create_metric_label'; import { LinkDescriptor } from '../../../../../hooks/use_link_props'; +/* + We've recently changed the default index pattern in Metrics UI from `metricbeat-*` to + `metrics-*,metricbeat-*`. There is a bug in TSVB when there is an empty index in the pattern + the field dropdowns are not populated correctly. This index pattern is a temporary fix. + See: https://github.com/elastic/kibana/issues/73987 +*/ +const TSVB_WORKAROUND_INDEX_PATTERN = 'metric*'; + export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptionsMetric) => { if (metric.aggregation === 'rate') { const metricId = uuid.v1(); @@ -128,6 +136,13 @@ export const createFilterFromOptions = ( return { language: 'kuery', query: filters.join(' and ') }; }; +const createTSVBIndexPattern = (alias: string) => { + if (alias.split(',').length > 1) { + return TSVB_WORKAROUND_INDEX_PATTERN; + } + return alias; +}; + export const createTSVBLink = ( source: SourceQuery.Query['source']['configuration'] | undefined, options: MetricsExplorerOptions, @@ -135,6 +150,9 @@ export const createTSVBLink = ( timeRange: MetricsExplorerTimeOptions, chartOptions: MetricsExplorerChartOptions ): LinkDescriptor => { + const tsvbIndexPattern = createTSVBIndexPattern( + (source && source.metricAlias) || TSVB_WORKAROUND_INDEX_PATTERN + ); const appState = { filters: [], linked: false, @@ -147,8 +165,8 @@ export const createTSVBLink = ( axis_position: 'left', axis_scale: 'normal', id: uuid.v1(), - default_index_pattern: (source && source.metricAlias) || 'metricbeat-*', - index_pattern: (source && source.metricAlias) || 'metricbeat-*', + default_index_pattern: tsvbIndexPattern, + index_pattern: tsvbIndexPattern, interval: 'auto', series: options.metrics.map(mapMetricToSeries(chartOptions)), show_grid: 1, diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 7acef263f973a..69bcc498c18be 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -22,7 +22,8 @@ export interface IngestManagerConfigType { host?: string; ca_sha256?: string; }; - agentConfigRolloutConcurrency: number; + agentConfigRolloutRateLimitIntervalMs: number; + agentConfigRolloutRateLimitRequestPerInterval: number; }; } diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md index a33d95f3afa38..bd20780611055 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/definitions.md +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -10,7 +10,7 @@ This section is to define terms used across ingest management. A package config is a definition on how to collect data from a service, for example `nginx`. A package config contains definitions for one or multiple inputs and each input can contain one or multiple streams. -With the example of the nginx Package Config, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected +With the example of the nginx Package Config, it contains two inputs: `logs` and `nginx/metrics`. Logs and metrics are collected differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. ## Data Stream 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 fe11c4cb08d13..8ea236b2dd6c3 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 0e65cb80f07c4..b87bd66cce0ea 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 ee03b3faf79d1..401211409ebf7 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/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 6f8c4948559d3..e2f659f54d625 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -35,7 +35,8 @@ export const config = { host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), }), - agentConfigRolloutConcurrency: schema.number({ defaultValue: 10 }), + agentConfigRolloutRateLimitIntervalMs: schema.number({ defaultValue: 5000 }), + agentConfigRolloutRateLimitRequestPerInterval: schema.number({ defaultValue: 5 }), }), }), }; 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 f54e61280b98a..f8f39f6294260 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 { }); // assertions for calls to .decrease are commented out because it's called on the -// "req.events.aborted$ observable (which) will never emit from a mocked request in a jest unit test environment" +// "req.events.completed$ observable (which) will never emit from a mocked request in a jest unit test environment" // https://github.com/elastic/kibana/pull/72338#issuecomment-661908791 describe('preAuthHandler', () => { test(`ignores routes when !isMatch`, async () => { diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts index 11fdc944e031d..7ba8e151b726c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts @@ -66,9 +66,7 @@ export function createLimitedPreAuthHandler({ maxCounter.increase(); - // requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes - // https://github.com/elastic/kibana/pull/70495#issuecomment-656288766 - request.events.aborted$.toPromise().then(() => { + request.events.completed$.toPromise().then(() => { maxCounter.decrease(); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts deleted file mode 100644 index 70207dcf325c4..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Rx from 'rxjs'; -import { share } from 'rxjs/operators'; -import { createSubscriberConcurrencyLimiter } from './rxjs_utils'; - -function createSpyObserver(o: Rx.Observable): [Rx.Subscription, jest.Mock] { - const spy = jest.fn(); - const observer = o.subscribe(spy); - return [observer, spy]; -} - -describe('createSubscriberConcurrencyLimiter', () => { - it('should not publish to more than n concurrent subscriber', async () => { - const subject = new Rx.Subject(); - const sharedObservable = subject.pipe(share()); - - const limiter = createSubscriberConcurrencyLimiter(2); - - const [observer1, spy1] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer2, spy2] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer3, spy3] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer4, spy4] = createSpyObserver(sharedObservable.pipe(limiter())); - subject.next('test1'); - - expect(spy1).toBeCalled(); - expect(spy2).toBeCalled(); - expect(spy3).not.toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer1.unsubscribe(); - expect(spy3).toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer2.unsubscribe(); - expect(spy4).toBeCalled(); - - observer3.unsubscribe(); - observer4.unsubscribe(); - }); -}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts index dc0ed35207e46..dddade6841460 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts @@ -43,23 +43,37 @@ export const toPromiseAbortable = ( } }); -export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { - let observers: Array<[Rx.Subscriber, any]> = []; - let activeObservers: Array> = []; +export function createRateLimiter( + ratelimitIntervalMs: number, + ratelimitRequestPerInterval: number +) { + function createCurrentInterval() { + return { + startedAt: Rx.asyncScheduler.now(), + numRequests: 0, + }; + } - function processNext() { - if (activeObservers.length >= maxConcurrency) { - return; - } - const observerValuePair = observers.shift(); + let currentInterval: { startedAt: number; numRequests: number } = createCurrentInterval(); + let observers: Array<[Rx.Subscriber, any]> = []; + let timerSubscription: Rx.Subscription | undefined; - if (!observerValuePair) { + function createTimeout() { + if (timerSubscription) { return; } - - const [observer, value] = observerValuePair; - activeObservers.push(observer); - observer.next(value); + timerSubscription = Rx.asyncScheduler.schedule(() => { + timerSubscription = undefined; + currentInterval = createCurrentInterval(); + for (const [waitingObserver, value] of observers) { + if (currentInterval.numRequests >= ratelimitRequestPerInterval) { + createTimeout(); + continue; + } + currentInterval.numRequests++; + waitingObserver.next(value); + } + }, ratelimitIntervalMs); } return function limit(): Rx.MonoTypeOperatorFunction { @@ -67,8 +81,14 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { new Rx.Observable((observer) => { const subscription = observable.subscribe({ next(value) { + if (currentInterval.numRequests < ratelimitRequestPerInterval) { + currentInterval.numRequests++; + observer.next(value); + return; + } + observers = [...observers, [observer, value]]; - processNext(); + createTimeout(); }, error(err) { observer.error(err); @@ -79,10 +99,8 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { }); return () => { - activeObservers = activeObservers.filter((o) => o !== observer); observers = observers.filter((o) => o[0] !== observer); subscription.unsubscribe(); - processNext(); }; }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 53270afe453c4..1547b6b5ea053 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -28,7 +28,7 @@ import * as APIKeysService from '../../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_UPDATE_ACTIONS_INTERVAL_MS } from '../../../constants'; import { createAgentAction, getNewActionsSince } from '../actions'; import { appContextService } from '../../app_context'; -import { toPromiseAbortable, AbortError, createSubscriberConcurrencyLimiter } from './rxjs_utils'; +import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -134,8 +134,9 @@ export function agentCheckinStateNewActionsFactory() { const agentConfigs$ = new Map>(); const newActions$ = createNewActionsSharedObservable(); // Rx operators - const concurrencyLimiter = createSubscriberConcurrencyLimiter( - appContextService.getConfig()?.fleet.agentConfigRolloutConcurrency ?? 10 + const rateLimiter = createRateLimiter( + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitIntervalMs ?? 5000, + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitRequestPerInterval ?? 50 ); async function subscribeToNewActions( @@ -158,7 +159,7 @@ export function agentCheckinStateNewActionsFactory() { const stream$ = agentConfig$.pipe( timeout(appContextService.getConfig()?.fleet.pollingRequestTimeout || 0), filter((config) => shouldCreateAgentConfigAction(agent, config)), - concurrencyLimiter(), + rateLimiter(), mergeMap((config) => createAgentActionFromConfig(soClient, agent, config)), merge(newActions$), mergeMap(async (data) => { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 878cecf644032..44e4eddfbbe6a 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -31,25 +31,38 @@ export const installPipelines = async ( // it can be created pointing to the new template, without removing the old one and effecting data // so do not remove the currently installed pipelines here const datasets = registryPackage.datasets; + if (!datasets?.length) return []; const pipelinePaths = paths.filter((path) => 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 436a6a1bdc55d..2a3120f064904 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/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index a3fe444b19b1a..5741764164b83 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 a69daae6e0410..1346b75c81e48 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 81bc5847e6c0e..1acf2131dcb01 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 c7f2df38fe41a..c701762e50b50 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 { 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'; @@ -249,6 +249,27 @@ describe('Lens App', () => { expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); }); + it('passes global filters to frame', async () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); + args.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { + return [pinnedFilter]; + }); + const component = mount(); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + dateRange: { fromDate: 'now-7d', toDate: 'now' }, + query: { query: '', language: 'kuery' }, + filters: [pinnedFilter], + }) + ); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); instance = mount(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 2a7eaff32fa08..ab4c4820315ac 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -94,7 +94,7 @@ export function App({ toDate: currentRange.to, }, originatingApp, - filters: [], + filters: data.query.filterManager.getFilters(), indicateNoData: false, }; }); 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 430e4983882cb..f9ef5b8fde5b5 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 15779d22681c0..7b184819b839b 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 410b38e79ffe4..2c190d54f0265 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 eb2bb94b36a69..7c8d08bacdb51 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 f4625e42ab5de..fc931b13619ef 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/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 23889bdca2dd7..f5f5071bab158 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -158,7 +158,7 @@ export class VectorLayer extends AbstractLayer { async getBounds({ startLoading, stopLoading, registerCancelCallback, dataFilters }) { const isStaticLayer = !this.getSource().isBoundsAware(); - if (isStaticLayer) { + if (isStaticLayer || this.hasJoins()) { return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()); } 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 7e8e6896ef9ce..9d5bf85005ae1 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 cfdbd728c2217..02ca4645dd8cd 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 5efba64360f23..0056a2cba02cd 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 478d96962e47b..a99548b6af7bf 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 4210b59f0d676..09241d538a0fe 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 ea3886c600be9..c5d41ae2b1a9b 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 e255dceda856e..d68bbdae2c17e 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 88eb4109627e5..4d50c632bfd6d 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 cde4d1f201199..e643abcaf8d54 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 2183a298a2841..425954c9af860 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 132c0b3f27603..81513818bc0ec 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 665317569e5e8..0d152534aba6a 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 662d1ccf33b95..83bd4b70ba5c3 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 216fde595af32..18b7faf6283c4 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 7a0ed4fb3e962..ec52f6a0f7283 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 137b3a31b795b..616d06a5c7b19 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 { - dispatch(setSelectedLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removePreviewLayers()); - }, - enableFullScreen: () => dispatch(enableFullScreen()), - openMapSettings: () => dispatch(openMapSettings()), - }; -} - -const connectedMapsTopNavMenu = connect(mapStateToProps, mapDispatchToProps)(MapsTopNavMenu); -export { connectedMapsTopNavMenu as MapsTopNavMenu }; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js index d7c754c91b89a..c5f959c54fb66 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js @@ -13,6 +13,7 @@ import { getQueryableUniqueIndexPatternIds, getRefreshConfig, getTimeFilters, + hasDirtyState, hasUnsavedChanges, } from '../../../selectors/map_selectors'; import { @@ -26,13 +27,20 @@ import { setRefreshConfig, setSelectedLayer, updateFlyout, + enableFullScreen, + openMapSettings, + removePreviewLayers, } from '../../../actions'; import { FLYOUT_STATE } from '../../../reducers/ui'; import { getMapsCapabilities } from '../../../kibana_services'; +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; function mapStateToProps(state = {}) { return { isFullScreen: getIsFullScreen(state), + isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + isSaveDisabled: hasDirtyState(state), + inspectorAdapters: getInspectorAdapters(state), nextIndexPatternIds: getQueryableUniqueIndexPatternIds(state), flyoutDisplay: getFlyoutDisplay(state), refreshConfig: getRefreshConfig(state), @@ -68,6 +76,13 @@ function mapDispatchToProps(dispatch) { dispatch(updateFlyout(FLYOUT_STATE.NONE)); dispatch(setReadOnly(!getMapsCapabilities().save)); }, + closeFlyout: () => { + dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removePreviewLayers()); + }, + enableFullScreen: () => dispatch(enableFullScreen()), + openMapSettings: () => dispatch(openMapSettings()), }; } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index d945aa9623b21..97a08f11a6757 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -9,13 +9,17 @@ import { i18n } from '@kbn/i18n'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; -import { getData, getCoreChrome } from '../../../kibana_services'; +import { + getData, + getCoreChrome, + getMapsCapabilities, + getNavigation, +} from '../../../kibana_services'; import { copyPersistentState } from '../../../reducers/util'; import { getInitialLayers, getInitialLayersFromUrlParam } from '../../bootstrap/get_initial_layers'; import { getInitialTimeFilters } from '../../bootstrap/get_initial_time_filters'; import { getInitialRefreshConfig } from '../../bootstrap/get_initial_refresh_config'; import { getInitialQuery } from '../../bootstrap/get_initial_query'; -import { MapsTopNavMenu } from '../../page_elements/top_nav_menu'; import { getGlobalState, updateGlobalState, @@ -27,6 +31,7 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; import { goToSpecifiedPath } from '../../maps_router'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; +import { getTopNavConfig } from './top_nav_config'; const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', @@ -58,7 +63,10 @@ export class MapsAppView extends React.Component { this._updateFromGlobalState ); - this._updateStateFromSavedQuery(this._appStateManager.getAppState().savedQuery); + const initialSavedQuery = this._appStateManager.getAppState().savedQuery; + if (initialSavedQuery) { + this._updateStateFromSavedQuery(initialSavedQuery); + } this._initMap(); @@ -237,18 +245,10 @@ export class MapsAppView extends React.Component { ); } - _onTopNavRefreshConfig = ({ isPaused, refreshInterval }) => { - this._onRefreshConfigChange({ - isPaused, - interval: refreshInterval, - }); - }; + _updateStateFromSavedQuery = (savedQuery) => { + this.setState({ savedQuery: { ...savedQuery } }); + this._appStateManager.setQueryAndFilters({ savedQuery }); - _updateStateFromSavedQuery(savedQuery) { - if (!savedQuery) { - this.setState({ savedQuery: '' }); - return; - } const { filterManager } = getData().query; const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -266,7 +266,7 @@ export class MapsAppView extends React.Component { query: savedQuery.attributes.query, time: savedQuery.attributes.timefilter, }); - } + }; _initMap() { this._initMapAndLayerSettings(); @@ -295,27 +295,65 @@ export class MapsAppView extends React.Component { } _renderTopNav() { - return !this.props.isFullScreen ? ( - { - this.setState({ savedQuery: query }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + filters={this.props.filters} + query={this.props.query} + onQuerySubmit={({ dateRange, query }) => { + this._onQueryChange({ + query, + time: dateRange, + refresh: true, + }); }} - onSavedQueryUpdated={(query) => { - this.setState({ savedQuery: { ...query } }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + onFiltersUpdated={this._onFiltersChange} + dateRangeFrom={this.props.timeFilters.from} + dateRangeTo={this.props.timeFilters.to} + isRefreshPaused={this.props.refreshConfig.isPaused} + refreshInterval={this.props.refreshConfig.interval} + onRefreshChange={({ isPaused, refreshInterval }) => { + this._onRefreshConfigChange({ + isPaused, + interval: refreshInterval, + }); + }} + showSearchBar={true} + showFilterBar={true} + showDatePicker={true} + showSaveQuery={getMapsCapabilities().saveQuery} + savedQuery={this.state.savedQuery} + onSaved={this._updateStateFromSavedQuery} + onSavedQueryUpdated={this._updateStateFromSavedQuery} + onClearSavedQuery={() => { + const { filterManager, queryString } = getData().query; + this.setState({ savedQuery: '' }); + this._appStateManager.setQueryAndFilters({ savedQuery: '' }); + this._onQueryChange({ + filters: filterManager.getGlobalFilters(), + query: queryString.getDefaultQuery(), + }); }} - setBreadcrumbs={this._setBreadcrumbs} /> - ) : null; + ); } render() { diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx similarity index 72% rename from x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js rename to x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx index be474b43da81a..46d662b28a82f 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx @@ -6,109 +6,44 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { Adapters } from 'src/plugins/inspector/public'; +import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { - getNavigation, getCoreChrome, getMapsCapabilities, getInspector, getToasts, getCoreI18n, - getData, } from '../../../kibana_services'; import { SavedObjectSaveModal, + OnSaveProps, showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +// @ts-expect-error import { goToSpecifiedPath } from '../../maps_router'; +import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; -export function MapsTopNavMenu({ +export function getTopNavConfig({ savedMap, - query, - onQueryChange, - onQuerySaved, - onSavedQueryUpdated, - savedQuery, - timeFilters, - refreshConfig, - onRefreshConfigChange, - indexPatterns, - onFiltersChange, + isOpenSettingsDisabled, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, setBreadcrumbs, - isOpenSettingsDisabled, +}: { + savedMap: ISavedGisMap; + isOpenSettingsDisabled: boolean; + isSaveDisabled: boolean; + closeFlyout: () => void; + enableFullScreen: () => void; + openMapSettings: () => void; + inspectorAdapters: Adapters; + setBreadcrumbs: () => void; }) { - const { TopNavMenu } = getNavigation().ui; - const { filterManager, queryString } = getData().query; - const showSaveQuery = getMapsCapabilities().saveQuery; - const onClearSavedQuery = () => { - onQuerySaved(undefined); - onQueryChange({ - filters: filterManager.getGlobalFilters(), - query: queryString.getDefaultQuery(), - }); - }; - - // Nav settings - const config = getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs - ); - - const submitQuery = function ({ dateRange, query }) { - onQueryChange({ - query, - time: dateRange, - refresh: true, - }); - }; - - return ( - - ); -} - -function getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs -) { return [ { id: 'full-screen', @@ -180,11 +115,11 @@ function getTopNavConfig( newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate, - }) => { + }: OnSaveProps) => { const currentTitle = savedMap.title; savedMap.title = newTitle; savedMap.copyOnSave = newCopyOnSave; - const saveOptions = { + const saveOptions: SavedObjectSaveOpts = { confirmOverwrite: false, isTitleDuplicateConfirmed, onTitleDuplicate, @@ -218,7 +153,12 @@ function getTopNavConfig( ]; } -async function doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs) { +async function doSave( + savedMap: ISavedGisMap, + saveOptions: SavedObjectSaveOpts, + closeFlyout: () => void, + setBreadcrumbs: () => void +) { closeFlyout(); savedMap.syncWithStore(); let id; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 0ac237bb33e76..1d6a603caa817 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -44,7 +44,7 @@ export const DetailsStepForm: FC = ({ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState } = actions; - const { form, cloneJob, isJobCreated } = state; + const { form, cloneJob, hasSwitchedToEditor, isJobCreated } = state; const { createIndexPattern, description, @@ -61,7 +61,9 @@ export const DetailsStepForm: FC = ({ resultsField, } = form; - const [destIndexSameAsId, setDestIndexSameAsId] = useState(cloneJob === undefined); + const [destIndexSameAsId, setDestIndexSameAsId] = useState( + cloneJob === undefined && hasSwitchedToEditor === false + ); const forceInput = useRef(null); @@ -90,7 +92,11 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destinationIndexNameValid === true) { debouncedIndexCheck(); - } else if (destinationIndex.trim() === '' && destinationIndexNameExists === true) { + } else if ( + typeof destinationIndex === 'string' && + destinationIndex.trim() === '' && + destinationIndexNameExists === true + ) { setFormState({ destinationIndexNameExists: false }); } @@ -102,7 +108,7 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destIndexSameAsId === true && !jobIdEmpty && jobIdValid) { setFormState({ destinationIndex: jobId }); - } else if (destIndexSameAsId === false) { + } else if (destIndexSameAsId === false && hasSwitchedToEditor === false) { setFormState({ destinationIndex: '' }); } }, [destIndexSameAsId, jobId]); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index 04dd25896d443..2f0e2ed3428c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -6,7 +6,6 @@ import React, { FC, useEffect, useState } from 'react'; import { - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -16,7 +15,7 @@ import { EuiSpacer, EuiSteps, EuiStepStatus, - EuiText, + EuiSwitch, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -48,9 +47,15 @@ export const Page: FC = ({ jobId }) => { const { currentIndexPattern } = mlContext; const createAnalyticsForm = useCreateAnalyticsForm(); - const { isAdvancedEditorEnabled } = createAnalyticsForm.state; - const { jobType } = createAnalyticsForm.state.form; - const { initiateWizard, setJobClone, switchToAdvancedEditor } = createAnalyticsForm.actions; + const { state } = createAnalyticsForm; + const { isAdvancedEditorEnabled, disableSwitchToForm } = state; + const { jobType } = state.form; + const { + initiateWizard, + setJobClone, + switchToAdvancedEditor, + switchToForm, + } = createAnalyticsForm.actions; useEffect(() => { initiateWizard(); @@ -170,34 +175,40 @@ export const Page: FC = ({ jobId }) => { - {isAdvancedEditorEnabled === false && ( - - + + - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch', - { - defaultMessage: 'Switch to json editor', - } - )} - - - - - )} + checked={isAdvancedEditorEnabled} + onChange={(e) => { + if (e.target.checked === true) { + switchToAdvancedEditor(); + } else { + switchToForm(); + } + }} + data-test-subj="mlAnalyticsCreateJobWizardAdvancedEditorSwitch" + /> + + {isAdvancedEditorEnabled === true && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx index f54cc4621eccf..e988640d2eae5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx @@ -15,7 +15,7 @@ export const RefreshAnalyticsListButton: FC = () => { const { refresh } = useRefreshAnalyticsList({ isLoading: setIsLoading }); return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 4bfee9f308313..5f3045696f170 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -25,6 +25,7 @@ export enum ACTION { SET_JOB_CONFIG, SET_JOB_IDS, SWITCH_TO_ADVANCED_EDITOR, + SWITCH_TO_FORM, SET_ESTIMATED_MODEL_MEMORY_LIMIT, SET_JOB_CLONE, } @@ -38,7 +39,8 @@ export type Action = | ACTION.OPEN_MODAL | ACTION.RESET_ADVANCED_EDITOR_MESSAGES | ACTION.RESET_FORM - | ACTION.SWITCH_TO_ADVANCED_EDITOR; + | ACTION.SWITCH_TO_ADVANCED_EDITOR + | ACTION.SWITCH_TO_FORM; } // Actions with custom payloads: | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: FormMessage } @@ -71,6 +73,7 @@ export interface ActionDispatchers { setJobConfig: (payload: State['jobConfig']) => void; startAnalyticsJob: () => void; switchToAdvancedEditor: () => void; + switchToForm: () => void; setEstimatedModelMemoryLimit: (value: State['estimatedModelMemoryLimit']) => void; setJobClone: (cloneJob: DeepReadonly) => Promise; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index acdaf15cdf4b7..8d8421a116b91 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -8,13 +8,17 @@ import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; // @ts-ignore import numeral from '@elastic/numeral'; -import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; import { Action, ACTION } from './actions'; -import { getInitialState, getJobConfigFromFormState, State } from './state'; +import { + getInitialState, + getFormStateFromJobConfig, + getJobConfigFromFormState, + State, +} from './state'; import { isJobIdValid, validateModelMemoryLimitUnits, @@ -41,6 +45,7 @@ import { TRAINING_PERCENT_MAX, } from '../../../../common/analytics'; import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public'; +import { isAdvancedConfig } from '../../components/action_clone/clone_button'; const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( ', ' @@ -458,13 +463,16 @@ export function reducer(state: State, action: Action): State { case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: let resultJobConfig; + let disableSwitchToForm = false; try { resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString)); + disableSwitchToForm = isAdvancedConfig(resultJobConfig); } catch (e) { return { ...state, advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: false, + disableSwitchToForm: true, advancedEditorMessages: [], }; } @@ -473,6 +481,7 @@ export function reducer(state: State, action: Action): State { ...validateAdvancedEditor({ ...state, jobConfig: resultJobConfig }), advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: true, + disableSwitchToForm, }; case ACTION.SET_FORM_STATE: @@ -538,17 +547,53 @@ export function reducer(state: State, action: Action): State { case ACTION.SWITCH_TO_ADVANCED_EDITOR: let { jobConfig } = state; - const isJobConfigEmpty = isEmpty(state.jobConfig); - if (isJobConfigEmpty) { - jobConfig = getJobConfigFromFormState(state.form); - } + jobConfig = getJobConfigFromFormState(state.form); + const shouldDisableSwitchToForm = isAdvancedConfig(jobConfig); + return validateAdvancedEditor({ ...state, advancedEditorRawString: JSON.stringify(jobConfig, null, 2), isAdvancedEditorEnabled: true, + disableSwitchToForm: shouldDisableSwitchToForm, + hasSwitchedToEditor: true, jobConfig, }); + case ACTION.SWITCH_TO_FORM: + const { jobConfig: config, jobIds } = state; + const { jobId } = state.form; + // @ts-ignore + const formState = getFormStateFromJobConfig(config, false); + + if (typeof jobId === 'string' && jobId.trim() !== '') { + formState.jobId = jobId; + } + + formState.jobIdExists = jobIds.some((id) => formState.jobId === id); + formState.jobIdEmpty = jobId === ''; + formState.jobIdValid = isJobIdValid(jobId); + formState.jobIdInvalidMaxLength = !!maxLengthValidator(JOB_ID_MAX_LENGTH)(jobId); + + formState.destinationIndexNameEmpty = formState.destinationIndex === ''; + formState.destinationIndexNameValid = isValidIndexName(formState.destinationIndex || ''); + formState.destinationIndexPatternTitleExists = + state.indexPatternsMap[formState.destinationIndex || ''] !== undefined; + + if (formState.numTopFeatureImportanceValues !== undefined) { + formState.numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues( + formState.numTopFeatureImportanceValues + ); + } + + return validateForm({ + ...state, + // @ts-ignore + form: formState, + isAdvancedEditorEnabled: false, + advancedEditorRawString: JSON.stringify(config, null, 2), + jobConfig: config, + }); + case ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT: return { ...state, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts index d397dfc315da4..499318ebddc19 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getCloneFormStateFromJobConfig, - getInitialState, - getJobConfigFromFormState, -} from './state'; +import { getFormStateFromJobConfig, getInitialState, getJobConfigFromFormState } from './state'; const regJobConfig = { id: 'reg-test-01', @@ -96,8 +92,8 @@ describe('useCreateAnalyticsForm', () => { ]); }); - test('state: getCloneFormStateFromJobConfig() regression', () => { - const clonedState = getCloneFormStateFromJobConfig(regJobConfig); + test('state: getFormStateFromJobConfig() regression', () => { + const clonedState = getFormStateFromJobConfig(regJobConfig); expect(clonedState?.sourceIndex).toBe('reg-test-index'); expect(clonedState?.includes).toStrictEqual([]); @@ -112,8 +108,8 @@ describe('useCreateAnalyticsForm', () => { expect(clonedState?.jobId).toBe(undefined); }); - test('state: getCloneFormStateFromJobConfig() outlier detection', () => { - const clonedState = getCloneFormStateFromJobConfig(outlierJobConfig); + test('state: getFormStateFromJobConfig() outlier detection', () => { + const clonedState = getFormStateFromJobConfig(outlierJobConfig); expect(clonedState?.sourceIndex).toBe('outlier-test-index'); expect(clonedState?.includes).toStrictEqual(['field', 'other_field']); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 725fc8751408e..69599f43ef297 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -12,6 +12,7 @@ import { DataFrameAnalyticsId, DataFrameAnalyticsConfig, ANALYSIS_CONFIG_TYPE, + defaultSearchQuery, } from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; @@ -44,6 +45,7 @@ export interface FormMessage { export interface State { advancedEditorMessages: FormMessage[]; advancedEditorRawString: string; + disableSwitchToForm: boolean; form: { computeFeatureInfluence: string; createIndexPattern: boolean; @@ -97,6 +99,7 @@ export interface State { indexPatternsMap: SourceIndexMap; isAdvancedEditorEnabled: boolean; isAdvancedEditorValidJson: boolean; + hasSwitchedToEditor: boolean; isJobCreated: boolean; isJobStarted: boolean; isValid: boolean; @@ -110,6 +113,7 @@ export interface State { export const getInitialState = (): State => ({ advancedEditorMessages: [], advancedEditorRawString: '', + disableSwitchToForm: false, form: { computeFeatureInfluence: 'true', createIndexPattern: true, @@ -131,7 +135,7 @@ export const getInitialState = (): State => ({ jobIdInvalidMaxLength: false, jobIdValid: false, jobType: undefined, - jobConfigQuery: { match_all: {} }, + jobConfigQuery: defaultSearchQuery, jobConfigQueryString: undefined, lambda: undefined, loadingFieldOptions: false, @@ -167,6 +171,7 @@ export const getInitialState = (): State => ({ indexPatternsMap: {}, isAdvancedEditorEnabled: false, isAdvancedEditorValidJson: true, + hasSwitchedToEditor: false, isJobCreated: false, isJobStarted: false, isValid: false, @@ -283,8 +288,9 @@ function toCamelCase(property: string): string { * Extracts form state for a job clone from the analytics job configuration. * For cloning we keep job id and destination index empty. */ -export function getCloneFormStateFromJobConfig( - analyticsJobConfig: Readonly +export function getFormStateFromJobConfig( + analyticsJobConfig: Readonly, + isClone: boolean = true ): Partial { const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; @@ -300,6 +306,10 @@ export function getCloneFormStateFromJobConfig( includes: analyticsJobConfig.analyzed_fields.includes, }; + if (isClone === false) { + resultState.destinationIndex = analyticsJobConfig?.dest.index ?? ''; + } + const analysisConfig = analyticsJobConfig.analysis[jobType]; for (const key in analysisConfig) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 035610684d556..9612b9213d120 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -28,7 +28,7 @@ import { FormMessage, State, SourceIndexMap, - getCloneFormStateFromJobConfig, + getFormStateFromJobConfig, } from './state'; import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; @@ -283,6 +283,10 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SWITCH_TO_ADVANCED_EDITOR }); }; + const switchToForm = () => { + dispatch({ type: ACTION.SWITCH_TO_FORM }); + }; + const setEstimatedModelMemoryLimit = (value: State['estimatedModelMemoryLimit']) => { dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); }; @@ -294,7 +298,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig(config); switchToAdvancedEditor(); } else { - setFormState(getCloneFormStateFromJobConfig(config)); + setFormState(getFormStateFromJobConfig(config)); setEstimatedModelMemoryLimit(config.model_memory_limit); } @@ -311,6 +315,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig, startAnalyticsJob, switchToAdvancedEditor, + switchToForm, setEstimatedModelMemoryLimit, setJobClone, }; 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 ecb8626867887..7c55ed01f432a 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/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx new file mode 100644 index 0000000000000..0dd802855ea67 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect, useMemo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiLoadingSpinner, + EuiButton, +} from '@elastic/eui'; + +import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; +import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; +import { mlJobService } from '../../../../../../services/job_service'; +import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; + +export const DatafeedPreview: FC<{ + combinedJob: CombinedJob | null; + heightOffset?: number; +}> = ({ combinedJob, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); + const [loading, setLoading] = useState(false); + const [previewJsonString, setPreviewJsonString] = useState(''); + const [outOfDate, setOutOfDate] = useState(false); + const [combinedJobString, setCombinedJobString] = useState(''); + + useEffect(() => { + try { + if (combinedJob !== null) { + if (combinedJobString === '') { + // first time, set the string and load the preview + loadDataPreview(); + } else { + setOutOfDate(JSON.stringify(combinedJob) !== combinedJobString); + } + } + } catch (error) { + // fail silently + } + }, [combinedJob]); + + const loadDataPreview = useCallback(async () => { + setPreviewJsonString(''); + if (combinedJob === null) { + return; + } + + setLoading(true); + setCombinedJobString(JSON.stringify(combinedJob)); + + if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { + try { + const resp = await mlJobService.searchPreview(combinedJob); + const data = resp.aggregations + ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) + : resp.hits.hits; + + setPreviewJsonString(JSON.stringify(data, null, 2)); + } catch (error) { + setPreviewJsonString(JSON.stringify(error, null, 2)); + } + setLoading(false); + setOutOfDate(false); + } else { + const errorText = i18n.translate( + 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', + { + defaultMessage: 'Datafeed does not exist', + } + ); + setPreviewJsonString(errorText); + } + }, [combinedJob]); + + return ( + + + + +
+ +
+ + + + {outOfDate && ( + + Refresh + + )} + + + + {loading === true ? ( + + + + + + + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx index 03be38adfbbe0..d35083ec6e479 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { Fragment, FC, useState, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -13,18 +12,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, - EuiTitle, EuiFlyoutBody, - EuiSpacer, - EuiLoadingSpinner, } from '@elastic/eui'; -import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; -import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; + import { JobCreatorContext } from '../../job_creator_context'; -import { mlJobService } from '../../../../../../services/job_service'; -import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; +import { DatafeedPreview } from './datafeed_preview'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, @@ -36,50 +29,11 @@ interface Props { export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { const { jobCreator } = useContext(JobCreatorContext); const [showFlyout, setShowFlyout] = useState(false); - const [previewJsonString, setPreviewJsonString] = useState(''); - const [loading, setLoading] = useState(false); function toggleFlyout() { setShowFlyout(!showFlyout); } - useEffect(() => { - if (showFlyout === true) { - loadDataPreview(); - } - }, [showFlyout]); - - async function loadDataPreview() { - setLoading(true); - setPreviewJsonString(''); - const combinedJob: CombinedJob = { - ...jobCreator.jobConfig, - datafeed_config: jobCreator.datafeedConfig, - }; - - if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { - try { - const resp = await mlJobService.searchPreview(combinedJob); - const data = resp.aggregations - ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) - : resp.hits.hits; - - setPreviewJsonString(JSON.stringify(data, null, 2)); - } catch (error) { - setPreviewJsonString(JSON.stringify(error, null, 2)); - } - setLoading(false); - } else { - const errorText = i18n.translate( - 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', - { - defaultMessage: 'Datafeed does not exist', - } - ); - setPreviewJsonString(errorText); - } - } - return ( @@ -87,12 +41,11 @@ export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { {showFlyout === true && isDisabled === false && ( setShowFlyout(false)} hideCloseButton size="m"> - @@ -127,28 +80,3 @@ const FlyoutButton: FC<{ isDisabled: boolean; onClick(): void }> = ({ isDisabled ); }; - -const Contents: FC<{ - title: string; - value: string; - loading: boolean; -}> = ({ title, value, loading }) => { - return ( - - -
{title}
-
- - {loading === true ? ( - - - - - - - ) : ( - - )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts index d52ed1364452a..e96f374213eb3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts @@ -4,3 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { DatafeedPreviewFlyout } from './datafeed_preview_flyout'; +export { DatafeedPreview } from './datafeed_preview'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index dd5c8aa3e280a..29d55e6ae48e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; +import React, { Fragment, FC, useState, useContext, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -17,19 +17,21 @@ import { EuiTitle, EuiFlyoutBody, EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import { collapseLiteralStrings } from '../../../../../../../../shared_imports'; -import { Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; +import { CombinedJob, Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; +import { DatafeedPreview } from '../datafeed_preview_flyout'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, EDITABLE, } +const WARNING_CALLOUT_OFFSET = 100; interface Props { isDisabled: boolean; jobEditorMode: EDITOR_MODE; @@ -38,21 +40,38 @@ interface Props { export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafeedEditorMode }) => { const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const [showJsonFlyout, setShowJsonFlyout] = useState(false); + const [showChangedIndicesWarning, setShowChangedIndicesWarning] = useState(false); const [jobConfigString, setJobConfigString] = useState(jobCreator.formattedJobJson); const [datafeedConfigString, setDatafeedConfigString] = useState( jobCreator.formattedDatafeedJson ); const [saveable, setSaveable] = useState(false); + const [tempCombinedJob, setTempCombinedJob] = useState(null); useEffect(() => { setJobConfigString(jobCreator.formattedJobJson); setDatafeedConfigString(jobCreator.formattedDatafeedJson); }, [jobCreatorUpdated]); + useEffect(() => { + if (showJsonFlyout === true) { + // when the flyout opens, update the JSON + setJobConfigString(jobCreator.formattedJobJson); + setDatafeedConfigString(jobCreator.formattedDatafeedJson); + setTempCombinedJob({ + ...JSON.parse(jobCreator.formattedJobJson), + datafeed_config: JSON.parse(jobCreator.formattedDatafeedJson), + }); + + setShowChangedIndicesWarning(false); + } else { + setTempCombinedJob(null); + } + }, [showJsonFlyout]); + const editJsonMode = - jobEditorMode === EDITOR_MODE.HIDDEN || datafeedEditorMode === EDITOR_MODE.HIDDEN; - const flyOutSize = editJsonMode ? 'm' : 'l'; + jobEditorMode === EDITOR_MODE.EDITABLE || datafeedEditorMode === EDITOR_MODE.EDITABLE; const readOnlyMode = jobEditorMode === EDITOR_MODE.READONLY && datafeedEditorMode === EDITOR_MODE.READONLY; @@ -64,6 +83,14 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee function onJobChange(json: string) { setJobConfigString(json); const valid = isValidJson(json); + setTempCombinedJob( + valid + ? { + ...JSON.parse(json), + datafeed_config: JSON.parse(datafeedConfigString), + } + : null + ); setSaveable(valid); } @@ -73,12 +100,22 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee let valid = isValidJson(jsonValue); if (valid) { // ensure that the user hasn't altered the indices list in the json. - const { indices }: Datafeed = JSON.parse(jsonValue); + const datafeed: Datafeed = JSON.parse(jsonValue); const originalIndices = jobCreator.indices.sort(); valid = - originalIndices.length === indices.length && - originalIndices.every((value, index) => value === indices[index]); + originalIndices.length === datafeed.indices.length && + originalIndices.every((value, index) => value === datafeed.indices[index]); + setShowChangedIndicesWarning(valid === false); + + setTempCombinedJob({ + ...JSON.parse(jobConfigString), + datafeed_config: datafeed, + }); + } else { + setShowChangedIndicesWarning(false); + setTempCombinedJob(null); } + setSaveable(valid); } @@ -99,7 +136,7 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee /> {showJsonFlyout === true && isDisabled === false && ( - setShowJsonFlyout(false)} hideCloseButton size={flyOutSize}> + setShowJsonFlyout(false)} hideCloseButton size={'l'}> {jobEditorMode !== EDITOR_MODE.HIDDEN && ( @@ -110,19 +147,51 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee defaultMessage: 'Job configuration JSON', })} value={jobConfigString} + heightOffset={showChangedIndicesWarning ? WARNING_CALLOUT_OFFSET : 0} /> )} {datafeedEditorMode !== EDITOR_MODE.HIDDEN && ( - + <> + + {datafeedEditorMode === EDITOR_MODE.EDITABLE && ( + + + + )} + )} + {showChangedIndicesWarning && ( + <> + + + + + + )} @@ -183,7 +252,12 @@ const Contents: FC<{ value: string; editJson: boolean; onChange(s: string): void; -}> = ({ title, value, editJson, onChange }) => { + heightOffset?: number; +}> = ({ title, value, editJson, onChange, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); return ( @@ -192,7 +266,7 @@ const Contents: FC<{ { + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); + const [confirmModalVisible, setConfirmModalVisible] = useState(false); + const [defaultQueryString] = useState(JSON.stringify(getDefaultDatafeedQuery(), null, 2)); + + const closeModal = () => setConfirmModalVisible(false); + const showModal = () => setConfirmModalVisible(true); + + function resetDatafeed() { + jobCreator.query = getDefaultDatafeedQuery(); + jobCreatorUpdate(); + closeModal(); + } + return ( + <> + {confirmModalVisible && ( + + + + + + + + {defaultQueryString} + + + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx index 7e895ca206849..b9250c3ecdce5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx @@ -11,11 +11,11 @@ import { QueryInput } from './components/query'; import { QueryDelayInput } from './components/query_delay'; import { FrequencyInput } from './components/frequency'; import { ScrollSizeInput } from './components/scroll_size'; +import { ResetQueryButton } from './components/reset_query'; import { TimeField } from './components/time_field'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); @@ -47,19 +47,13 @@ export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) = + setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} nextActive={nextActive}> - - - - - - - - +
)} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx index b0fb2e7267f7f..bff99ad7c5281 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx @@ -14,6 +14,8 @@ import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { AdvancedSection } from './components/advanced_section'; import { AdditionalSection } from './components/additional_section'; +import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; +import { isAdvancedJobCreator } from '../../../common/job_creator'; interface Props extends StepProps { advancedExpanded: boolean; @@ -30,7 +32,7 @@ export const JobDetailsStep: FC = ({ additionalExpanded, setAdditionalExpanded, }) => { - const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); + const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); useEffect(() => { @@ -70,7 +72,15 @@ export const JobDetailsStep: FC = ({ previous={() => setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} next={() => setCurrentStep(WIZARD_STEPS.VALIDATION)} nextActive={nextActive} - /> + > + {isAdvancedJobCreator(jobCreator) && ( + + )} + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index 6f03b9a3c3321..2316383709164 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -6,23 +6,26 @@ import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { JobCreatorContext } from '../job_creator_context'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { SingleMetricView } from './components/single_metric_view'; import { MultiMetricView } from './components/multi_metric_view'; import { PopulationView } from './components/population_view'; import { AdvancedView } from './components/advanced_view'; import { CategorizationView } from './components/categorization_view'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; +import { + isSingleMetricJobCreator, + isMultiMetricJobCreator, + isPopulationJobCreator, + isCategorizationJobCreator, + isAdvancedJobCreator, +} from '../../../common/job_creator'; export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); - const jobType = jobCreator.type; useEffect(() => { setNextActive(jobValidator.isPickFieldsStepValid); @@ -32,25 +35,25 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) {isCurrentStep && ( - {jobType === JOB_TYPE.SINGLE_METRIC && ( + {isSingleMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.MULTI_METRIC && ( + {isMultiMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.POPULATION && ( + {isPopulationJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.ADVANCED && ( + {isAdvancedJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.CATEGORIZATION && ( + {isCategorizationJobCreator(jobCreator) && ( )} setCurrentStep( - jobCreator.type === JOB_TYPE.ADVANCED + isAdvancedJobCreator(jobCreator) ? WIZARD_STEPS.ADVANCED_CONFIGURE_DATAFEED : WIZARD_STEPS.TIME_RANGE ) @@ -58,19 +61,12 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) next={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} nextActive={nextActive} > - {jobType === JOB_TYPE.ADVANCED && ( - - - - - - - - + {isAdvancedJobCreator(jobCreator) && ( + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 5ef59951c43cc..24d7fb9fc2a40 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -22,8 +22,6 @@ import { JobCreatorContext } from '../job_creator_context'; import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { getErrorMessage } from '../../../../../../../common/util/errors'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; @@ -54,13 +52,14 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const [jobRunner, setJobRunner] = useState(null); const isAdvanced = isAdvancedJobCreator(jobCreator); + const jsonEditorMode = isAdvanced ? EDITOR_MODE.EDITABLE : EDITOR_MODE.READONLY; useEffect(() => { jobCreator.subscribeToProgress(setProgress); }, []); async function start() { - if (jobCreator.type === JOB_TYPE.ADVANCED) { + if (isAdvanced) { await startAdvanced(); } else { await startInline(); @@ -176,15 +175,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => 0} - jobEditorMode={EDITOR_MODE.READONLY} - datafeedEditorMode={EDITOR_MODE.READONLY} + jobEditorMode={jsonEditorMode} + datafeedEditorMode={jsonEditorMode} /> - {jobCreator.type === JOB_TYPE.ADVANCED ? ( - - - - ) : ( + {isAdvanced === false && ( - ) : ( - - - - ); -} - export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => { const { services: { @@ -59,7 +42,7 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

@@ -69,7 +52,6 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled } /> ), - createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), transforms: ( = (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 7754af1be8588..6dcfa6dd043aa 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 df0070176727e..3591ad178f4cd 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 edf4c5d73f837..34e995510cf72 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 eb1f82eb5550d..6fa533302db48 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 7c9758bc0ddb6..9b4a50271a247 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 b7463fe6532b7..43512f8e528f6 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 caa21e5e69292..4a1137079ebb4 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 2eaa25803c81e..0d9e50d14657b 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 cc58d7267f6dc..44ee883c135d2 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 618547398bd96..9b4b086a0208b 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 eda32cd39c0d0..d682e87b7ca95 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 a36b945e82ef7..b99093a3d8ad1 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 cfac5e195a127..88b36b9572fc7 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 2f88245d88c4a..a41d4ec4bbfa2 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 cac57f599633d..016acf2737f9b 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 f25179fa63c2f..4b083787f58cb 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 2596252c92d11..c330e977e53d8 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 4742f55487045..afe5abcf1ebd7 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 50bf40825c515..ed300c211215b 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 1a76fae9fc420..dd3b37b5755e7 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 0f677dcc9c120..e2f21b34efe21 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 f29c199b3f1e1..fbb4a01d5b4ed 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 d45d404b38304..4b3e3d2d6cb6d 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/lib/setup/collection/__test__/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js index e56627369475b..083ebfb27fd51 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js @@ -10,7 +10,12 @@ import { getCollectionStatus } from '..'; import { getIndexPatterns } from '../../../cluster/get_index_patterns'; const liveClusterUuid = 'a12'; -const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = true) => { +const mockReq = ( + searchResult = {}, + securityEnabled = true, + userHasPermissions = true, + securityErrorMessage = null +) => { return { server: { newPlatform: { @@ -37,12 +42,14 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = }, }, plugins: { - xpack_main: { + monitoring: { info: { - isAvailable: () => true, - feature: () => ({ - isEnabled: () => securityEnabled, - }), + getSecurityFeature: () => { + return { + isAvailable: securityEnabled, + isEnabled: securityEnabled, + }; + }, }, }, elasticsearch: { @@ -61,6 +68,11 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = params && params.path === '/_security/user/_has_privileges' ) { + if (securityErrorMessage !== null) { + return Promise.reject({ + message: securityErrorMessage, + }); + } return Promise.resolve({ has_all_requested: userHasPermissions }); } if (type === 'transport.request' && params && params.path === '/_nodes') { @@ -245,6 +257,34 @@ describe('getCollectionStatus', () => { expect(result.kibana.detected.doesExist).to.be(true); }); + it('should work properly with an unknown security message', async () => { + const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar'); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result._meta.hasPermissions).to.be(false); + }); + + it('should work properly with a known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'no handler found for uri [/_security/user/_has_privileges] and method [POST]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + + it('should work properly with another known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'Invalid index name [_security]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + it('should not work if the user does not have the necessary permissions', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, false); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 607503673276b..81cdfd6ecd172 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -233,6 +233,10 @@ function isBeatFromAPM(bucket) { } async function hasNecessaryPermissions(req) { + const securityFeature = req.server.plugins.monitoring.info.getSecurityFeature(); + if (!securityFeature.isAvailable || !securityFeature.isEnabled) { + return true; + } try { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); const response = await callWithRequest(req, 'transport.request', { @@ -250,6 +254,9 @@ async function hasNecessaryPermissions(req) { ) { return true; } + if (err.message.includes('Invalid index name [_security]')) { + return true; + } return false; } } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 86022a0e863d5..ed091d4b8d7a7 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 1e7a5acb33644..a0ef6d3e2d984 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 32bdb00577bd2..8870bcbc9fa38 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 eb16a9d6de1a8..494f7ab0a28db 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 157d109e9e27e..809bfb57dd4fa 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 cebcd228b01c3..29eb51dff21da 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 e7fb0c6e2cb99..9acfc6d8c608d 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 f2ce423566c46..2e0292e1f9ab5 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 f9a9d9d85bfd3..1042fd66abad7 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 0000000000000..1b5e73648c2f4 --- /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 1b72be6c92f43..0ad41cd904853 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 ab4dabf9ed2c2..c6d3d826c88fb 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 db10d96db2263..08313e6892f3c 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 62f923aa6d793..6e6e0f443015b 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 216b5cc028588..1168b5edb6ffd 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 0000000000000..10f9ebb5623df --- /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 a30fddc3c3a69..aeadc34c6e49c 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 bc0740554bc52..af4fe7257ae5b 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 ec57e142125da..1c0102382ab6b 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 789759643e319..4f382f13bcd5d 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 88ae582b58891..de9d343bc91f7 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 809498d25c5d8..2e1d3379dc202 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 41b9252c67b8a..7c287646ba7ac 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 88969c3ae5fb3..f697ce443f2c5 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 a54cf142c18b7..87bd7bb247056 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 0c1da8694bf1a..733e3db3c25e6 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 e30560f6c8147..841a1ef09ede6 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 832b14f00159a..633135d63ac33 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 8617388f4ffb5..64c8fde87a6bc 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 fba8c3faa9237..cafb0095431f1 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 af40d4ff18d5a..00a4e581320bb 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 e836e2e20432a..436386077e725 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 c402116ee2714..e4520dab4626a 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 e6eaa4947e404..7526c52d16fde 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 0000000000000..3ce2f704b3643 --- /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 0000000000000..b766a0536d237 --- /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 0000000000000..e63f95064cba0 --- /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 0f54ec29cc540..2a116c4cd8acf 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 3883a2fad2cf2..3044f6d01b745 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 7624ce147abd9..e90639a2c0285 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 50a6150833796..cd8b66acd223a 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 224c99756eb5c..a3c5d09a0fb64 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 8585f58504e31..f6b703b7e622e 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 0000000000000..3fa0e59f9acb0 --- /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 b82607a541aaa..165f3314c2f15 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 0000000000000..ee5bd1329f35b --- /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 6109b85f2da5a..e1352ac38dc49 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 9fda60b1f289d..d9032092744a1 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 80e7209492fa5..c324b812a9ec2 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 a1e7293ce974b..fbc3d62768d00 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 d5d670b4c03ff..038c116c9fc8c 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 3dc120b3d874d..435f3f6e349d0 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 e78f148418588..cd660ae611610 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 fa512ad1ed80b..e93ade7191f52 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 78cd23e6647c0..9bfae686b1a59 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 73fe7b1ea5f6e..af27d411b990d 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 8cb35fc689185..4cfb7f8ad2b5b 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 8bf0690bfd0ad..140429dc4abd7 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 03f9b43678003..373c1f7aaec75 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 e1b192df104d7..1bcbebd12b9bc 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 aa0d90a216035..32591fb032436 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 0000000000000..546d2615fe6ae --- /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 4a3d734d0a6d4..c34027648c896 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 16d1a1481bc96..c2b51e29c230d 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 f38a9107afca9..be9725dac5ff3 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 ab95e433d92f3..d93bad29f3348 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 e5e8635b9e799..3d6c3dc0a7a8e 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 0b341050fa9d5..47c12d1934174 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/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts index 49da7dbf6d514..9b0cec99b1b38 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts @@ -17,7 +17,7 @@ export const PRE_BUILT_MSG = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage', { defaultMessage: - 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', + 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules except the Elastic Endpoint Security rule are disabled. You can select additional rules you want to activate.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index 35816e82540d1..0f16cb99862a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -70,7 +70,7 @@ export const RiskScoreField = ({ { field: newField?.name ?? '', operator: 'equals', - value: undefined, + value: '', riskScore: undefined, }, ], 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 20470d7bb924f..a3db8fe659d84 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 939747717385c..f4d90d0596ede 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 7063dca2341ca..9beebdfb923dc 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/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 46829b9cb8f7b..f58c95ed71e29 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -20,6 +20,7 @@ import { getPrePackagedRulesStatus, } from './api'; import { ruleMock, rulesMock } from './mock'; +import { buildEsQuery } from 'src/plugins/data/common'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -165,7 +166,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { method: 'GET', query: { - filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', + filter: 'alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', page: 1, per_page: 20, sort_field: 'enabled', @@ -175,6 +176,75 @@ describe('Detections Rules API', () => { }); }); + test('query with tags KQL parses without errors when tags contain characters such as left parenthesis (', async () => { + await fetchRules({ + filterOptions: { + filter: 'ruleName', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['('], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + + test('query KQL parses without errors when filter contains characters such as double quotes', async () => { + await fetchRules({ + filterOptions: { + filter: '"test"', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: [], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + + test('query KQL parses without errors when tags contains characters such as double quotes', async () => { + await fetchRules({ + filterOptions: { + filter: '"test"', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['"test"'], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + test('check parameter url, query with all options', async () => { await fetchRules({ filterOptions: { @@ -191,7 +261,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: hello AND alert.attributes.tags: world', + 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', page: 1, per_page: 20, sort_field: 'enabled', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 08d564230b85f..3538d8ec8c9b9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -97,7 +97,7 @@ export const fetchRules = async ({ ...(filterOptions.showElasticRules ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), - ...(filterOptions.tags?.map((t) => `alert.attributes.tags: ${t}`) ?? []), + ...(filterOptions.tags?.map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) ?? []), ]; const query = { 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 c114e4519df10..d76da592e1c81 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 789469e981fb4..4327ef96c93a7 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 84096e242cbbd..7b20873bf63cc 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 90d1b8bd54df5..f7d2c81f536be 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 b6c1727ee6afa..34840b2826626 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 1d0b73f80a69a..e4e69443c510d 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 962c227d6b670..cea987db485f4 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 = ({ { const { name, ...queryParams } = props; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index 355c2bb5c19fc..b69e5c5cd0e65 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -140,7 +140,7 @@ const hostListApiPathHandlerMocks = ({ // Build a GET route handler for each host details based on the list of Hosts passed on input if (hostsResults) { hostsResults.forEach((host) => { - // @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/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 58442ab417b60..f91bba3e3125a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -263,6 +263,7 @@ export const HostList = () => { render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { const toRoutePath = getHostDetailsPath({ name: 'hostPolicyResponse', + ...queryParams, selected_host: item.metadata.host.id, }); const toRouteUrl = formatUrl(toRoutePath); 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 e7aa2c8893f8e..43a6ad2c585b4 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 c58e53d07acba..25928197590ea 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 42469a4bf29da..c9ac1fc7a6e9c 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 f516f2a2de346..601bae89f7a4d 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 0000000000000..99902a31975d0 --- /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 40969a6e1df4a..875a678f32226 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 1b743c259555a..520fd6c459705 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/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 7a61427c56a3b..2a2354921a3d4 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -251,6 +251,20 @@ export class Simulator { return this.findInDOM('[data-test-subj="resolver:graph"]'); } + /** + * The outer panel container. + */ + public panelElement(): ReactWrapper { + return this.findInDOM('[data-test-subj="resolver:panel"]'); + } + + /** + * The panel content element (which may include tables, lists, other data depending on the view). + */ + public panelContentElement(): ReactWrapper { + return this.findInDOM('[data-test-subj^="resolver:panel:"]'); + } + /** * Like `this.wrapper.find` but only returns DOM nodes. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 9cb900736677e..f339d128944cc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -63,6 +63,16 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( expect(simulator.processNodeElements().length).toBe(3); }); + it(`should have the default "process list" panel present`, async () => { + expect(simulator.panelElement().length).toBe(1); + expect(simulator.panelContentElement().length).toBe(1); + const testSubjectName = simulator + .panelContentElement() + .getDOMNode() + .getAttribute('data-test-subj'); + expect(testSubjectName).toMatch(/process-list/g); + }); + describe("when the second child node's first button has been clicked", () => { beforeEach(() => { // Click the first button under the second child element. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 83d3930065da6..f378ab36bac94 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -220,7 +220,7 @@ PanelContent.displayName = 'PanelContent'; export const Panel = memo(function Event({ className }: { className?: string }) { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index efb96cde431e5..8ca002ace26fe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -187,7 +187,12 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ {showWarning && } - items={processTableView} columns={columns} sorting /> + + data-test-subj="resolver:panel:process-list" + items={processTableView} + columns={columns} + sorting + /> ); }); 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 4bf0033bcb430..46c9fbb524066 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 d30fd6f31012c..f24ef3448d03f 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 3f842bcc2eb68..7528468ef6522 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 97d1d11395c7f..ededf70152968 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 957b37a0bd1c2..7d083735e6c71 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 f4bd17005fed7..ac6c61b33b35e 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 af289f94c9a0d..5b5c2c9eeafc0 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 6c1c88f511edb..facdc392ff7ba 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,45 +4,94 @@ * 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'; + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + useHistory: jest.fn(), + }; +}); + +jest.mock('./helpers', () => { + const originalModule = jest.requireActual('./helpers'); + return { + ...originalModule, + queryTimelineById: jest.fn(), + }; +}); -import { StatefulOpenTimeline } from '.'; -import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; -jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/all', () => { const originalModule = jest.requireActual('../../containers/all'); return { ...originalModule, useGetAllTimeline: jest.fn(), - getAllTimeline: originalModule.getAllTimeline, }; }); -jest.mock('./use_timeline_types', () => { + +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, + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }), + }; +}); + +jest.mock('./use_timeline_status', () => { return { - useTimelineTypes: jest.fn().mockReturnValue({ - timelineType: 'default', - timelineTabs:
, - timelineFilters:
, - }), + 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, + pageName: SecurityPageName.timelines, + }); + mockHistory = []; + (useHistory as jest.Mock).mockReturnValue(mockHistory); ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), timelines: getAllTimeline( @@ -53,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', () => { @@ -85,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( @@ -433,10 +599,7 @@ describe('StatefulOpenTimeline', () => { }); }); - /** - * enable this test when createtTemplateTimeline is ready - */ - test.skip('it renders the tabs', async () => { + test('it has the expected initial state for openTimeline - templateTimelineFilter', () => { const wrapper = mount( @@ -451,11 +614,31 @@ describe('StatefulOpenTimeline', () => { ); - await waitFor(() => { - expect( - wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists() - ).toEqual(true); - }); + expect(wrapper.find('[data-test-subj="open-timeline-subtabs"]').exists()).toEqual(true); + }); + + test('it has the expected initial state for openTimelineModalBody - templateTimelineFilter', () => { + const wrapper = mount( + + + + + + ); + + 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/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 188b8979f613c..4c5db80a6c916 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -326,6 +326,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineTabs} title={title} totalSearchResultsCount={totalCount} @@ -356,6 +357,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineFilters} title={title} totalSearchResultsCount={totalCount} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 57a6431a06b90..9de3242c5e303 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from './timelines_table'; import { mockTimelineResults } from '../../../common/mock/timeline_results'; import { OpenTimeline } from './open_timeline'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/lib/kibana'); @@ -50,6 +50,7 @@ describe('OpenTimeline', () => { sortField: DEFAULT_SORT_FIELD, title, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], totalSearchResultsCount: mockSearchResults.length, }); @@ -263,4 +264,136 @@ describe('OpenTimeline', () => { `Showing: ${mockResults.length} timelines with "How was your day?"` ); }); + + test("it should render bulk actions if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); + + test("it should not render bulk actions if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(false); + }); + + test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate']); + }); + + test("it should not render selected count if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(false); + }); + + test("it should render bulk actions if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index d839a1deddf21..c9495c46d4acf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiBasicTable } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ImportDataModal } from '../../../common/components/import_data_modal'; import { UtilityBarGroup, @@ -55,6 +55,7 @@ export const OpenTimeline = React.memo( setImportDataModalToggle, sortField, timelineType = TimelineType.default, + timelineStatus, timelineFilter, templateTimelineFilter, totalSearchResultsCount, @@ -140,19 +141,23 @@ export const OpenTimeline = React.memo( }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); const actionTimelineToShow = useMemo(() => { - const timelineActions: ActionTimelineToShow[] = [ - 'createFrom', - 'duplicate', - 'export', - 'selectable', - ]; + const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate']; - if (onDeleteSelected != null && deleteTimelines != null) { + if (timelineStatus !== TimelineStatus.immutable) { + timelineActions.push('export'); + timelineActions.push('selectable'); + } + + if ( + onDeleteSelected != null && + deleteTimelines != null && + timelineStatus !== TimelineStatus.immutable + ) { timelineActions.push('delete'); } return timelineActions; - }, [onDeleteSelected, deleteTimelines]); + }, [onDeleteSelected, deleteTimelines, timelineStatus]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); @@ -206,20 +211,24 @@ export const OpenTimeline = React.memo( - - - {timelineType === TimelineType.template - ? i18n.SELECTED_TEMPLATES(selectedItems.length) - : i18n.SELECTED_TIMELINES(selectedItems.length)} - - - {i18n.BATCH_ACTIONS} - + {timelineStatus !== TimelineStatus.immutable && ( + <> + + {timelineType === TimelineType.template + ? i18n.SELECTED_TEMPLATES(selectedItems.length) + : i18n.SELECTED_TIMELINES(selectedItems.length)} + + + {i18n.BATCH_ACTIONS} + + + )} {i18n.REFRESH} 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 3017f553d59d5..5ce53607817eb 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/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 12df17ceba666..9632b0e6ecea4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from '../timelines_table'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineModalBody } from './open_timeline_modal_body'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; jest.mock('../../../../common/lib/kibana'); @@ -48,6 +48,7 @@ describe('OpenTimelineModal', () => { sortDirection: DEFAULT_SORT_DIRECTION, sortField: DEFAULT_SORT_FIELD, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], title, totalSearchResultsCount: mockSearchResults.length, 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 5b927db3c37a9..69f79fb7aece3 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/timelines_table/actions_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx index eddfdf6e01df2..52b7a4293e847 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../../../common/mock/match_media'; + import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTableProps } from '.'; @@ -233,4 +234,32 @@ describe('#getActionsColumns', () => { expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]); }); + + test('it should not render "export timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toEqual(false); + }); + + test('it should not render "delete timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toEqual(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx index 2d3672b15dd10..d2fba696d9d54 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx @@ -24,7 +24,7 @@ import { getActionsColumns } from './actions_columns'; import { getCommonColumns } from './common_columns'; import { getExtendedColumns } from './extended_columns'; import { getIconHeaderColumns } from './icon_header_columns'; -import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline'; +import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline'; // there are a number of type mismatches across this file const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -159,7 +159,8 @@ export const TimelinesTable = React.memo( }; const selection = { - selectable: (timelineResult: OpenTimelineResult) => timelineResult.savedObjectId != null, + selectable: (timelineResult: OpenTimelineResult) => + timelineResult.savedObjectId != null && timelineResult.status !== TimelineStatus.immutable, selectableMessage: (selectable: boolean) => !selectable ? i18n.MISSING_SAVED_OBJECT_ID : undefined, onSelectionChange, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index eb5a03baad88c..8950f814d6965 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -14,6 +14,7 @@ import { TimelineStatus, TemplateTimelineTypeLiteral, RowRendererId, + TimelineStatusLiteralWithNull, } from '../../../../common/types/timeline'; /** The users who added a timeline to favorites */ @@ -174,6 +175,8 @@ export interface OpenTimelineProps { sortField: string; /** this affects timeline's behaviour like editable / duplicatible */ timelineType: TimelineTypeLiteralWithNull; + /* active or immutable */ + timelineStatus: TimelineStatusLiteralWithNull; /** when timelineType === template, templatetimelineFilter is a JSX.Element */ templateTimelineFilter: JSX.Element[] | null; /** timeline / timeline template */ 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 8c4c686698c88..37bea3d713c08 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 7d54bb2209850..ec02124ae43d3 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 @@ -7,28 +7,35 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/link_to'; import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; -export const useTimelineTypes = ({ - defaultTimelineCount, - templateTimelineCount, -}: { +export interface UseTimelineTypesArgs { defaultTimelineCount?: number | null; templateTimelineCount?: number | null; -}): { +} + +export interface UseTimelineTypesResult { timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; timelineFilters: JSX.Element[]; -} => { +} + +export const useTimelineTypes = ({ + defaultTimelineCount, + templateTimelineCount, +}: UseTimelineTypesArgs): UseTimelineTypesResult => { const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); - const { tabName } = useParams<{ pageName: string; tabName: string }>(); + 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( @@ -61,7 +68,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? defaultTimelineCount ?? undefined : undefined, - onClick: goToTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, }, { id: TimelineType.template, @@ -76,7 +83,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? templateTimelineCount ?? undefined : undefined, - onClick: goToTemplateTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], [ @@ -93,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); } @@ -106,9 +113,10 @@ export const useTimelineTypes = ({ const timelineTabs = useMemo(() => { return ( <> - + {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.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts index c8adaa891610a..f4dc691f3d059 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts @@ -9,6 +9,7 @@ import { Ecs } from '../../../../graphql/types'; import { eventHasNotes, eventIsPinned, + getPinOnClick, getPinTooltip, stringifyEvent, isInvestigateInResolverActionEnabled, @@ -298,4 +299,72 @@ describe('helpers', () => { expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); }); }); + + describe('getPinOnClick', () => { + const eventId = 'abcd'; + + test('it invokes `onPinEvent` with the expected eventId when the event is NOT pinned, and allowUnpinning is true', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = true; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onPinEvent` when the event is NOT pinned, and allowUnpinning is false', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = false; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).not.toBeCalled(); + }); + + test('it invokes `onUnPinEvent` with the expected eventId when the event is pinned, and allowUnpinning is true', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = true; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onUnPinEvent` when the event is pinned, and allowUnpinning is false', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = false; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).not.toBeCalled(); + }); + }); }); 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 6a5e25632c29b..b62888fbf8427 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 @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty, noop } from 'lodash/fp'; + +import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; @@ -65,11 +66,16 @@ export const getPinOnClick = ({ onPinEvent, onUnPinEvent, isEventPinned, -}: GetPinOnClickParams): (() => void) => { +}: GetPinOnClickParams) => { if (!allowUnpinning) { - return noop; + return; + } + + if (isEventPinned) { + onUnPinEvent(eventId); + } else { + onPinEvent(eventId); } - return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId); }; /** @@ -129,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 5a98263cbd3fd..456c1ee541471 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 e971dc6c8e1e2..6f578ffe3e956 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 550a4adf713c6..15fa13b1a08f1 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 4e1595eef984c..3169201b12c7b 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/pin/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx index 657976e2f4787..2ca27ded86c9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getPinIcon } from './'; +import { mount } from 'enzyme'; +import React from 'react'; + +import { TimelineType } from '../../../../../common/types/timeline'; + +import { getPinIcon, Pin } from './'; + +interface ButtonIcon { + isDisabled: boolean; +} describe('pin', () => { describe('getPinRotation', () => { @@ -16,4 +25,62 @@ describe('pin', () => { expect(getPinIcon(false)).toEqual('pin'); }); }); + + describe('disabled button behavior', () => { + test('the button is enabled when allowUnpinning is true, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = true; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(false); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = false; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is true, and timelineType is `template`', () => { + const allowUnpinning = true; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is `template`', () => { + const allowUnpinning = false; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx index 30fe8ae0ca1f6..27780c7754d00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx @@ -34,7 +34,7 @@ export const Pin = React.memo( iconSize={iconSize} iconType={getPinIcon(pinned)} onClick={onClick} - isDisabled={isTemplate} + isDisabled={isTemplate || !allowUnpinning} /> ); } 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 3a01df8f48a3f..cc979816f0141 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 8f548f16cf1d6..5b3bc72fc37ca 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 f418491ac4e47..97f3b1df011ff 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/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index b59f9e90f8e74..c22acf6ba7cc1 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -33,7 +33,7 @@ const TimelinesContainer = styled.div` export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; export const TimelinesPageComponent: React.FC = () => { - const { tabName } = useParams(); + const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [importDataModalToggle, setImportDataModalToggle] = useState(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); 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 faeef432ea422..ebee0f5cae9a6 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 7757794c6dc9a..ad849c3a995b3 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 084f892369b51..161a31e2ec934 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 f3b832de9a786..29624b35d5c9e 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 266d522e8a41d..e9eb7093a7631 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 f6385d2710047..ba9be96201dbe 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 0000000000000..a4b6b0750ec10 --- /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 0000000000000..86f6d1a9a65e2 --- /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 d99533e23f2c2..902d287a09e42 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 33011078ee823..02cddc3ddcf6e 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 01dd59b2611d9..78e4219aad75c 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 d3ca7a54c16d2..ef487897e3b4e 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 8aaf809405d63..ab610dc9776ca 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 5f816b9ada54e..10faa362363ad 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/source_status/resolvers.test.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts new file mode 100644 index 0000000000000..1735c6473bb3a --- /dev/null +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { filterIndexes } from './resolvers'; + +describe('resolvers', () => { + test('it should filter single index that has an empty string', () => { + const emptyArray = filterIndexes(['']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has blanks within it', () => { + const emptyArray = filterIndexes([' ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that has an empty string and a valid index', () => { + const emptyArray = filterIndexes(['', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that have blanks within them and a valid index', () => { + const emptyArray = filterIndexes([' ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter single index that has _all within it', () => { + const emptyArray = filterIndexes(['_all']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has _all within it surrounded by spaces', () => { + const emptyArray = filterIndexes([' _all ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that _all within them and a valid index', () => { + const emptyArray = filterIndexes(['_all', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that _all surrounded with spaces within them and a valid index', () => { + const emptyArray = filterIndexes([' _all ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts index 8d55e645d6791..84320b1699531 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts @@ -32,27 +32,34 @@ export const createSourceStatusResolvers = (libs: { }; } => ({ SourceStatus: { - async indicesExist(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indicesExist(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.sourceStatus.hasIndices(req, indexes); + } else { return false; } - return libs.sourceStatus.hasIndices(req, args.defaultIndex); }, - async indexFields(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indexFields(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.fields.getFields(req, indexes); + } else { return []; } - return libs.fields.getFields(req, args.defaultIndex); }, }, }); +/** + * Given a set of indexes this will remove anything that is: + * - blank or empty strings are removed as not valid indexes + * - _all is removed as that is not a valid index + * @param indexes Indexes with invalid values removed + */ +export const filterIndexes = (indexes: string[]): string[] => + indexes.filter((index) => index.trim() !== '' && index.trim() !== '_all'); + export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({ name: 'IFieldSubType', description: 'Represents value in index pattern field item', 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 f4a18a40f7d4b..9bd544f6942cf 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 58a13a7115b72..573539e1bb54f 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 ca0732816aa4d..fa55af351651e 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 ee39661ee9b10..acc6f7724d0b5 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 4132d03c27854..25274928aa2b7 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 eb8523b797ddf..a2267755c7376 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 ddc9e91782136..4c79be6fe9045 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 a2936f3f09519..6be1f037f967e 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 ecbf268550b6c..5b73f849dddc5 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 87f20525203f6..2cf24d54b7268 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 f570b7fb3e946..ef20746fb1d80 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 9abbe3de148dd..d5b069f7b81e7 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 861821d24b73c..b22b74ebc53bc 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 431d133845f0e..e2ba81da917b3 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 13dd405c79326..4f4a9aacd79aa 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 67fb0b2e6755a..5bcc4a00ccd82 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 f60dede360b4b..a17fd6d2702dd 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 7c6ede8df7346..cf09bc512916f 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 78f4c9e853f64..b5e76b6ebfa36 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 f412ad9b2e2fd..6ba9503edc260 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 b76ea0944f855..3d31eead43c8f 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 353067e6db833..22ceb35dfc856 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 b70aa5cd11b52..95e357e56fe3b 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 ba9f43651e32f..0c82444dd9397 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 79c2d4c25b7d5..c76c5f20fa88b 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 b9727e18dddcf..b38ed94e132e1 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 e8f5f1a8de1c5..229a03de39600 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 2b45f059ec8d9..4800e87c180e2 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 a1b0ec0f01d2a..809a9a187937d 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 21ce4e498ccaf..8467b87f9983c 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 056de9e5c003e..075dd13d9819b 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 814caee4e888a..133863f8e2148 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 6426f8722df3d..85d348bb14be0 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 b27dfced0f4f6..38482c0a70fc9 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 d7da758e57c6d..7db683caf2bb2 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 30d482e9b9569..1c4666955dde0 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 4aad56abd0534..c375ea7b19b37 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 c630ad1eecec0..22090e1a241e7 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 989eff90aaf02..c2590a2f062cc 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 3c1ea7ee229c9..00491937e9aae 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 7202d9be3b8c3..16a398011fc53 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 f055ee44efb39..11781cb719599 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 afa1467b15074..7d931725fa6eb 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 801b60a2572e2..1bffe7a1cfc24 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 b1e8d0cd0d3e1..13d0eb267f640 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 3166cc23ae726..f3cc5c2eec8a3 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 730879684a811..334276142ca42 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 b2092dc78b012..ef7667e34be3a 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 ccec76b7f7974..1e8e1bfa42246 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 14472f02280a3..0e4bea426c591 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 a2fe82c43b15a..6ac2bbf355961 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 94f09f73b454e..e73aa5f4566a7 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 a7833c4a01751..0017186787139 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 05601ec8ffb4c..1466b4526815b 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 97197be498a8d..0ba6480fe42a1 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 832ca1e1e7d39..2d3edb0f5f6cc 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 e92ee45c0f3b6..3a4b4915f3c8b 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 c75f77301e531..a2eb76b9831f0 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 9b50d99761ad2..e43ab9de86ef7 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 192e35df1da3f..9d480259d49de 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 cb098086e3324..cdef5f16e5cd7 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 9f1d2fc62fadf..d501bda08c3a5 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 db96fe1bc1b50..e82b42869e44d 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 a5ac6cffd2376..e4c84fd3c3b83 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 59be6da19e93f..3aa9ac20bba9e 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 262313782fe33..0a1ba97bd01ea 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 6f9170f476d90..7305247192f57 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 1b5fd4e1f502d..7ff8eb9424d5f 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 f874b7e3f8e80..e923407765f8f 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 35206d130ea5f..24a744ce30832 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 43f1f8a5c9c61..529f2199e46dc 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 b49d1b358cb8d..69a25b3b24bac 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 2c141da80e797..cae5d1b7e0f1f 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 a9f8ee1af8bf6..5e3e44604e9b1 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 25711afbb4c66..81d82670e794a 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 8b627c48d2904..6bc14f4e5af8a 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 27e50313c8f82..ee434efa019d6 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 0bafa56c9af49..2de24a8155254 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 74b5e0d93c441..9fe0d97ceda32 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 59c659117c098..085acb9a2fb13 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 10a1989ad6423..f75e4d15f1e6a 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 4aa0b355171fe..68ad10977f4d3 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 25b300d33cce1..aab2deff3a266 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 9ca8b7ed21acb..abcc6f65fbc67 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 e8343f1b7b7c6..b0615cf032386 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 8c4387e60d281..d77533e5183ad 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 829d87c1964c9..026e1e549b574 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 7429c69fc3174..bd20be0924d05 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 25bf7dd287d05..2344346c8d61d 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 b4850e77ae719..8a68b26abad20 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 27e5da09452e7..2ea75dbd758cb 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 0273800c18d52..4379759608aba 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 a842d8ef952ff..24104439cd0ec 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 c1ce773c2aa44..73bf20a5a175e 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 98b262edfe6f6..1895caf4dea81 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 30d34f245c6d2..ac46bcbdbc083 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 57f5fe57b0e0b..2825dc28ad18f 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 086492edeb8ad..234a09e9607b9 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 09680fcf8e996..759622804444e 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 057d8ba9859a8..cd38aff3f2164 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 3dd18c8242a5e..7fcb9f915c560 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 1d15db83bb18e..b4c2d6522fb01 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 6df2ed6cb34a4..f64db94fbc7b2 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 e276166f6130b..30e52eed86110 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 bdfe7d25092ba..18a72a331219e 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 e3e0d5fef7b2f..b9c6e390effd9 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 ad21ebe065f8c..786fdd1ac16c0 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 e92cf3d67d313..06089272f0e8c 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 d5f3995fb8bcc..a9e6d2feef813 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 c5d8e50d3dba7..3392a1bff23b8 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 5f6c006c5d177..49b9a7501a3aa 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 d3a66ef8d9c77..f289e8341a0d0 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 7104cace1c5d9..9393f7e4ef515 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 c38f71d8e00a6..09aeed65f1ef0 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 99bb07fe9660e..229286d4e234d 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 9b2478b97fb38..b62384f5bd76a 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 48ed65caceda7..e76379d171bf7 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 b99690f78b2b4..b9e7f941ee5df 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 94a695a97a27a..830d2d956125b 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 24ea80e10f5e3..0cf6fcdb3875a 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 c3684006a49e5..59715dae441f4 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 5704f6d14bfec..7465751d5cd49 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 74c5376100b2b..aff6df969d90b 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 3738c04346e6e..9550eea6ca6aa 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 58dcd2d671f52..343426953add6 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 9850d4d908b69..44b50c74bafe6 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 d8b59804fecdf..50692dae3856f 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 bc80953d0aa61..8f938c0ceee6d 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 7ce54b00f211c..e271f855e4424 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 3dd8e7f1097f4..f3b8a81f4086a 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 335d1b8c86696..05a9e0bd1ac97 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 0c0694c0591f9..13184ac6c6d56 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 36862b519130b..87eddfbe6b9d4 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 513d6a93d1b5b..95ec753c21fd8 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 8c39a254e4261..3334cc17b9050 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 80950335934f4..fb1d51364ab39 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 a3c4f47b491be..c0a62a2cc887d 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 ee0d98c45c443..10dcb7ee7e743 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/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts index 944fc588afc8a..bb0a4b9e2ba9b 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts @@ -17,26 +17,21 @@ import { import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { FieldsAdapter, IndexFieldDescriptor } from './types'; -type IndexesAliasIndices = Record; - export class ElasticsearchIndexFieldAdapter implements FieldsAdapter { constructor(private readonly framework: FrameworkAdapter) {} public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { const indexPatternsService = this.framework.getIndexPatternsService(request); - const indexesAliasIndices: IndexesAliasIndices = indices.reduce( - (accumulator: IndexesAliasIndices, indice: string) => { - const key = getIndexAlias(indices, indice); + const indexesAliasIndices = indices.reduce>((accumulator, indice) => { + const key = getIndexAlias(indices, indice); - if (get(key, accumulator)) { - accumulator[key] = [...accumulator[key], indice]; - } else { - accumulator[key] = [indice]; - } - return accumulator; - }, - {} as IndexesAliasIndices - ); + if (get(key, accumulator)) { + accumulator[key] = [...accumulator[key], indice]; + } else { + accumulator[key] = [indice]; + } + return accumulator; + }, {}); const responsesIndexFields: IndexFieldDescriptor[][] = await Promise.all( Object.values(indexesAliasIndices).map((indicesByGroup) => indexPatternsService.getFieldsForWildcard({ 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 6493a3e05bfc9..6249e60d9a2be 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 bf6090f0337f7..0b043d4e2fdd0 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 b54d12d7efce3..f888675b60410 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 0b10018de5bba..245146dda183f 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 e3aeff280678f..c5d69398b7f0c 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 5bc4bec45dfb2..7abcb390d0221 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 30528f8563ab8..6f99739ae2e2b 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 c4957b9d4b9e2..f36adb648cc03 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 b817896e901c1..2ad6c5d6fff60 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 2c6098bc75500..65c956ed60440 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 a622ee9b15706..07ce9a7336d4d 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 2ce2c37d4fa31..b5aa24336b2d7 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 cdedffbbd9458..6bdecb5d80ecc 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 6f194c3b8538e..79ebf6280a19e 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 b926819d66c92..e358ad9dbb57d 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 1dac773ad6fde..0000000000000 --- 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 996dc5823691d..f62f02cc7bba9 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 3c4343b642891..0ef83bb84c4c3 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 6bc0ca64ae33f..23ea3e6213469 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 90839f5ac01c4..2a15f1fe074f8 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 37a97c03ad332..000bd875930f9 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/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts index 56ceca2b70e9c..5f002aa7fad7b 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts @@ -401,10 +401,17 @@ describe('Schema Beat', () => { const result = getIndexAlias([leadingWildcardIndex], leadingWildcardIndex); expect(result).toBe(leadingWildcardIndex); }); + test('getIndexAlias no match returns "unknown" string', () => { const index = 'auditbeat-*'; const result = getIndexAlias([index], 'hello'); expect(result).toBe('unknown'); }); + + test('empty index should not cause an error to return although it will cause an invalid regular expression to occur', () => { + const index = ''; + const result = getIndexAlias([index], 'hello'); + expect(result).toBe('unknown'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts index ff7331cf39bc7..6ec15d328714d 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts @@ -77,10 +77,16 @@ const convertFieldsToAssociativeArray = ( : {}; export const getIndexAlias = (defaultIndex: string[], indexName: string): string => { - const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); - if (found != null) { - return found; - } else { + try { + const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); + if (found != null) { + return found; + } else { + return 'unknown'; + } + } catch (error) { + // if we encounter an error because the index contains invalid regular expressions then we should return an unknown + // rather than blow up with a toaster error upstream return 'unknown'; } }; 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 f8a1f84937326..f886b0b461482 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 c81aade2b063e..aa12e2a34075e 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": "条件", @@ -11084,7 +11084,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "編集", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "Kibanaインデックスパターンの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "JSONエディターからこのフォームには戻れません。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", @@ -12362,9 +12361,7 @@ "xpack.ml.overview.feedbackSectionLink": "オンラインでのフィードバック", "xpack.ml.overview.feedbackSectionText": "ご利用に際し、ご意見やご提案がありましたら、{feedbackLink}までお送りください。", "xpack.ml.overview.feedbackSectionTitle": "フィードバック", - "xpack.ml.overview.gettingStartedSectionCreateJob": "新規ジョブを作成中", "xpack.ml.overview.gettingStartedSectionDocs": "ドキュメンテーション", - "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}や{createJob}をご参照ください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。", "xpack.ml.overview.gettingStartedSectionTitle": "はじめて使う", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearchの変換", "xpack.ml.overview.overviewLabel": "概要", @@ -14036,7 +14033,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": "サーバー", @@ -14324,7 +14320,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}」、終了します", @@ -19307,7 +19302,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 aba5adf72c2f8..befce4de3d247 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": "条件", @@ -11086,7 +11086,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "编辑", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误:", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "您不能从 json 编辑器切回到此表单。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", @@ -12365,9 +12364,7 @@ "xpack.ml.overview.feedbackSectionLink": "在线反馈", "xpack.ml.overview.feedbackSectionText": "如果您在体验方面有任何意见或建议,请提交{feedbackLink}。", "xpack.ml.overview.feedbackSectionTitle": "反馈", - "xpack.ml.overview.gettingStartedSectionCreateJob": "创建新作业", "xpack.ml.overview.gettingStartedSectionDocs": "文档", - "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先阅读我们的{docs}或{createJob}。建议使用 {transforms} 为分析作业创建功能索引。", "xpack.ml.overview.gettingStartedSectionTitle": "入门", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearch 的转换", "xpack.ml.overview.overviewLabel": "概览", @@ -14041,7 +14038,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": "服务器", @@ -14329,7 +14325,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}”,正在退出", @@ -19314,7 +19309,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 15099242b6e17..eb6b1ada3ba93 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 8ba909beff2a8..f5578aa5271be 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 3735942ff97af..db624688f9c76 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 b1dd78ff59f34..6ee7915e2be71 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 eaced3e27c8a0..6b7d693bb308e 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 2d62bca75c1a1..36dfdb55b4ab6 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 fd0d03dc18410..7c43ac0bbe56f 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/api_integration/apis/ml/annotations/common_jobs.ts b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts new file mode 100644 index 0000000000000..873cdc5d71baa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; + +export const commonJobConfig = { + description: 'test_job_annotation', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, +}; + +export const createJobConfig = (jobId: string) => { + return { ...commonJobConfig, job_id: jobId }; +}; + +export const testSetupJobConfigs = [1, 2, 3, 4].map((num) => ({ + ...commonJobConfig, + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, +})); +export const jobIds = testSetupJobConfigs.map((j) => j.job_id); + +export const createAnnotationRequestBody = (jobId: string): Partial => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; +}; + +export const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) +); diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts new file mode 100644 index 0000000000000..14ecf1bfe524e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { createJobConfig, createAnnotationRequestBody } from './common_jobs'; +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `job_annotation_${Date.now()}`; + const testJobConfig = createJobConfig(jobId); + const annotationRequestBody = createAnnotationRequestBody(jobId); + + describe('create_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createAnomalyDetectionJob(testJobConfig); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should successfully create annotations for anomaly job', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + const annotationId = body._id; + + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + + expect(fetchedAnnotation).to.not.be(undefined); + + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER); + }); + + it('should successfully create annotation for user with ML read permissions', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + + const annotationId = body._id; + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + expect(fetchedAnnotation).to.not.be(undefined); + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_VIEWER); + }); + + it('should not allow to create annotation for unauthorized user', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts new file mode 100644 index 0000000000000..4fbb26e9b5a3e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('delete_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should delete annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should delete annotation by id for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should not delete annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + await ml.api.waitForAnnotationToExist(annotationIdToDelete); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts new file mode 100644 index 0000000000000..710473eed6901 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { omit } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch all annotations for jobId', async () => { + const requestBody = { + jobIds: [jobIds[0]], + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + [jobIds[0]].forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for multiple jobs', async () => { + const requestBody = { + jobIds, + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for user with ML read permissions', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should not allow to fetch annotation for unauthorized user', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/index.ts b/x-pack/test/api_integration/apis/ml/annotations/index.ts new file mode 100644 index 0000000000000..7d73ee43d4d99 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/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 ({ loadTestFile }: FtrProviderContext) { + describe('annotations', function () { + loadTestFile(require.resolve('./create_annotations')); + loadTestFile(require.resolve('./get_annotations')); + loadTestFile(require.resolve('./delete_annotations')); + loadTestFile(require.resolve('./update_annotations')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts new file mode 100644 index 0000000000000..ba73617151120 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const commonAnnotationUpdateRequestBody: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', + }; + + describe('update_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should correctly update annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should correctly update annotation for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should not update annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + expect(updatedAnnotation).to.eql(originalAnnotation._source); + }); + + it('should override fields correctly', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[3]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBodyWithMissingFields: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + _id: originalAnnotation._id, + }; + await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBodyWithMissingFields) + .expect(200); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(annotationUpdateRequestBodyWithMissingFields).forEach((key) => { + if (key !== '_id') { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql( + annotationUpdateRequestBodyWithMissingFields[field] + ); + } + }); + } + // validate missing fields in the annotationUpdateRequestBody + expect(updatedAnnotation?.partition_field_name).to.be(undefined); + expect(updatedAnnotation?.partition_field_value).to.be(undefined); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index b29bc47b50394..969f291b0d8b3 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -60,5 +60,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./data_frame_analytics')); loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./calendars')); + loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index a9bbf09a9e6f9..f99dd4c65fc83 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -18,22 +18,97 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); - it('Make sure that we get source information when auditbeat indices is there', () => { - return client - .query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [], - }, - }) - .then((resp) => { - const sourceStatus = resp.data.source.status; - // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz - expect(sourceStatus.indexFields.length).to.be(397); - expect(sourceStatus.indicesExist).to.be(true); - }); + it('Make sure that we get source information when auditbeat indices is there', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz + expect(sourceStatus.indexFields.length).to.be(397); + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should find indexes as being available when they exist', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should not find indexes as existing when there is an empty array of them', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there is a _all within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['_all'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are empty strings within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [''], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are blank spaces within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [' '], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should find indexes when one is an empty index but the others are valid', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['', 'auditbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); }); }); } diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index c8adb3ce67d55..f1da94febc631 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -14,8 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const config = getService('config'); - // FLAKY: https://github.com/elastic/kibana/issues/57413 - describe.skip('spaces feature controls', () => { + describe('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); 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 553cceef573e1..0c4097a1d5c4e 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 20ad045d0a653..33db56751285e 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/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index c2500dca78444..68e5045c1f36c 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -129,17 +129,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show venn when clicking a line', async function () { await buildGraph(); - const { edges } = await PageObjects.graph.getGraphObjects(); await PageObjects.graph.isolateEdge('test', '/test/wp-admin/'); await PageObjects.graph.stopLayout(); await PageObjects.common.sleep(1000); - const testTestWpAdminBlogEdge = edges.find( - ({ sourceNode, targetNode }) => - targetNode.label === '/test/wp-admin/' && sourceNode.label === 'test' - )!; - await testTestWpAdminBlogEdge.element.click(); + await browser.execute(() => { + const event = document.createEvent('SVGEvents'); + event.initEvent('click', true, true); + return document.getElementsByClassName('gphEdge')[0].dispatchEvent(event); + }); await PageObjects.common.sleep(1000); await PageObjects.graph.startLayout(); diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index 64c07273c9ccf..c8e8db84df96f 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -25,10 +25,30 @@ export default function ({ getPageObjects }) { await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "ios"'); await PageObjects.maps.waitForMapPanAndZoom(origView); - const { lat, lon, zoom } = await PageObjects.maps.getView(); + const { lat, lon } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(43); expect(Math.round(lon)).to.equal(-102); - expect(Math.round(zoom)).to.equal(5); + }); + }); + + describe('with joins', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('join example'); + await PageObjects.maps.enableAutoFitToBounds(); + }); + + it('should automatically fit to bounds when query is applied', async () => { + // Set view to other side of world so no matching results + await PageObjects.maps.setView(0, 0, 6); + + // Setting query should trigger fit to bounds and move map + const origView = await PageObjects.maps.getView(); + await PageObjects.maps.setAndSubmitQuery('prop1 >= 11'); + await PageObjects.maps.waitForMapPanAndZoom(origView); + + const { lat, lon } = await PageObjects.maps.getView(); + expect(Math.round(lat)).to.equal(0); + expect(Math.round(lon)).to.equal(60); }); }); }); 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 d01883e9ea549..3564c72cfc348 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 c383d8593a4fa..6a2037bbc4924 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 0000000000000..95bd866d386b1 --- /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/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index ccf35a1e63e37..7e9a2cd85935e 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,8 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/70493 - describe.skip('certificates', function () { + describe('certificates', function () { before(async () => { await makeCheck({ es, tls: true }); await uptime.goToRoot(true); @@ -58,6 +57,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const certId = getSha256(); const { monitorId } = await makeCheck({ es, + monitorId: 'cert-test-check-id', + fields: { + monitor: { + name: 'Cert Test Check', + }, + url: { + full: 'https://site-to-check.com/', + }, + }, tls: { sha256: certId, }, diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 028ab3ff8803a..6b2b61cba2b64 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -55,6 +55,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./certificates')); }); + describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); 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 0000000000000..c434eee5dd8d3 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 0000000000000..1432a53b45461 --- /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 17f4df74921bc..fa10c5a574c09 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 f08d1e6b7fef4..23b5057573b3b 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 4b6342758be93..6d8eade25d7e6 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/api.ts b/x-pack/test/functional/services/ml/api.ts index 9dfec3a17dec0..401a96c5c11bd 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,13 +5,29 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { IndexDocumentParams } from 'elasticsearch'; import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; +import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; +import { + ML_ANNOTATIONS_INDEX_ALIAS_READ, + ML_ANNOTATIONS_INDEX_ALIAS_WRITE, +} from '../../../../plugins/ml/common/constants/index_patterns'; + +interface EsIndexResult { + _index: string; + _id: string; + _version: number; + result: string; + _shards: any; + _seq_no: number; + _primary_term: number; +} export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -634,5 +650,77 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } }); }, + + async getAnnotations(jobId: string) { + log.debug(`Fetching annotations for job '${jobId}'...`); + + const results = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + query: { + match: { + job_id: jobId, + }, + }, + }, + }); + expect(results).to.not.be(undefined); + expect(results).to.have.property('hits'); + return results.hits.hits; + }, + + async getAnnotationById(annotationId: string): Promise { + log.debug(`Fetching annotation '${annotationId}'...`); + + const result = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + size: 1, + query: { + match: { + _id: annotationId, + }, + }, + }, + }); + // @ts-ignore due to outdated type for hits.total + if (result.hits.total.value === 1) { + return result?.hits?.hits[0]?._source as Annotation; + } + return undefined; + }, + + async indexAnnotation(annotationRequestBody: Partial) { + log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); + // @ts-ignore due to outdated type for IndexDocumentParams.type + const params: IndexDocumentParams> = { + index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, + body: annotationRequestBody, + refresh: 'wait_for', + }; + const results: EsIndexResult = await es.index(params); + await this.waitForAnnotationToExist(results._id); + return results; + }, + + async waitForAnnotationToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) !== undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should exist`); + } + }); + }, + + async waitForAnnotationNotToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) === undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should not exist`); + } + }); + }, }; } 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 f452c9cce7a1a..d315f9eb77210 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 a72d9c204060b..58a1afad88e11 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 0087cbaae2b08..0187a3f976837 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 0000000000000..a71ad924a852f --- /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 453dca904b605..37d8b6e51072f 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/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index 2ceab1ca89e54..06de9be5af7e9 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -7,10 +7,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export function UptimeCertProvider({ getService }: FtrProviderContext) { +export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'timePicker', 'header']); + const changeSearchField = async (text: string) => { const input = await testSubjects.find('uptimeCertSearch'); await input.clearValueWithKeyboard(); @@ -61,6 +63,7 @@ export function UptimeCertProvider({ getService }: FtrProviderContext) { const self = this; return retry.tryForTime(60 * 1000, async () => { await changeSearchField(monId); + await PageObjects.header.waitUntilLoadingHasFinished(); await self.hasCertificates(1); }); }, diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index ab511abf130a5..710923c886cbe 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -17,7 +17,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv if (await testSubjects.exists('uptimeSettingsToOverviewLink', { timeout: 0 })) { await testSubjects.click('uptimeSettingsToOverviewLink'); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); - } else if (!(await testSubjects.exists('uptimeOverviewPage', { timeout: 0 }))) { + } else { await PageObjects.common.navigateToApp('uptime'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 6cb74aff95be2..a6de87d6f7b1a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -8,8 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - // FLAKY: https://github.com/elastic/kibana/issues/65948 - describe.skip('uptime alerts', () => { + describe('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); @@ -105,7 +104,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { alertTypeId, consumer, id, - params: { numTimes, timerange, locations, filters }, + params: { numTimes, timerangeUnit, timerangeCount, filters }, schedule: { interval }, tags, } = alert; @@ -119,14 +118,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(interval).to.eql('11m'); expect(tags).to.eql(['uptime', 'another']); expect(numTimes).to.be(3); - expect(timerange.from).to.be('now-1h'); - expect(timerange.to).to.be('now'); - expect(locations).to.eql(['mpls']); - expect(filters).to.eql( - '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + - '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + expect(timerangeUnit).to.be('h'); + expect(timerangeCount).to.be(1); + expect(JSON.stringify(filters)).to.eql( + `{"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"],"tags":[]}` ); } finally { await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); 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 0000000000000..1582f72dd1cd8 --- /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 9ca8ebf136078..35058de0684b2 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 0000000000000..9de6cd9118fe4 --- /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 98b26c1c04ebb..20414fcb90521 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 0000000000000..12a9a03c1337b --- /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 0000000000000..9ac3c68a0be9e --- /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 0000000000000..13ef3f4fa9152 --- /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 0000000000000..32c626b115739 --- /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 0000000000000..12a9a03c1337b --- /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 0000000000000..9ac3c68a0be9e --- /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 0000000000000..8e26522d86839 --- /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 0000000000000..773903a69e7f7 --- /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 0000000000000..12a9a03c1337b --- /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 0000000000000..9ac3c68a0be9e --- /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 0000000000000..8e26522d86839 --- /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 0000000000000..49c85994d2c2c --- /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 d21b80bd6eed7..72121b2164bfd 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 3a3e72e5bc237..930c90da5637c 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 4b517f0b970ce..18ef28150c42d 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 0000000000000..9f9800cafb99a --- /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 9cdef1c938890..8b4a73b7eb848 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 719327e5f9b79..3afa9f397a2ea 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 0000000000000..cde1a3616b620 --- /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 231871fae3d39..cb6c49e17c712 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 0000000000000..dc9a1fab9ec02 --- /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 3527e7e575c99..7b511c3be74b5 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 4e9f732f1e0a0..0638267afd256 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" @@ -4808,7 +4803,7 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest-specific-snapshot@^0.5.3": +"@types/jest-specific-snapshot@^0.5.3", "@types/jest-specific-snapshot@^0.5.4": version "0.5.4" resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.4.tgz#997364c39a59ddeff0ee790a19415e79dd061d1e" integrity sha512-1qISn4fH8wkOOPFEx+uWRRjw6m/pP/It3OHLm8Ee1KQpO7Z9ZGYDtWPU5AgK05UXsNTAgOK+dPQvJKGdy9E/1g== @@ -5726,7 +5721,7 @@ "@types/node" "*" chokidar "^2.1.2" -"@types/webpack-env@^1.15.0": +"@types/webpack-env@^1.15.0", "@types/webpack-env@^1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== @@ -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== @@ -11933,6 +11928,11 @@ diagnostics@^1.1.1: enabled "1.0.x" kuler "1.0.x" +diff-match-patch@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-match-patch@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" @@ -19267,6 +19267,14 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsondiffpatch@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/jsondiffpatch/-/jsondiffpatch-0.4.1.tgz#9fb085036767f03534ebd46dcd841df6070c5773" + integrity sha512-t0etAxTUk1w5MYdNOkZBZ8rvYYN5iL+2dHCCx/DpkFm/bW28M6y5nUS83D4XdZiHy35Fpaw6LBb+F88fHZnVCw== + dependencies: + chalk "^2.3.0" + diff-match-patch "^1.0.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"